mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
Compare commits
357
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7faabb569 | ||
|
|
3c7c5e1a78 | ||
|
|
fe4ba57892 | ||
|
|
389de8426c | ||
|
|
aad8ff9326 | ||
|
|
7b5395c5d4 | ||
|
|
82fdb0d95a | ||
|
|
43cb65ff90 | ||
|
|
e6283c3ca3 | ||
|
|
b71a3106ce | ||
|
|
4b3f10b2ab | ||
|
|
587abdd45f | ||
|
|
743484768b | ||
|
|
c2c593f437 | ||
|
|
1a24ede388 | ||
|
|
080610ac5f | ||
|
|
a09f751c14 | ||
|
|
3d8dc5fa0d | ||
|
|
4e301a5fff | ||
|
|
dfffc14a54 | ||
|
|
9c6d7ab4f1 | ||
|
|
847a274cdb | ||
|
|
e3f5ae0be7 | ||
|
|
3b8b3cb3dc | ||
|
|
e514070a64 | ||
|
|
7a08c2da2d | ||
|
|
ceb92ed289 | ||
|
|
c58b2eb6eb | ||
|
|
2c29756038 | ||
|
|
f11255da05 | ||
|
|
101a71d38e | ||
|
|
207d241b39 | ||
|
|
d14a723191 | ||
|
|
d4ef1c8bf0 | ||
|
|
1ae3176172 | ||
|
|
4626d35aa6 | ||
|
|
e707d6f92d | ||
|
|
451858654e | ||
|
|
ea95f7d911 | ||
|
|
a513d8e6eb | ||
|
|
a528ab6bea | ||
|
|
bb0c35aa20 | ||
|
|
4191e85c7b | ||
|
|
ab753cf9e2 | ||
|
|
864f7cd431 | ||
|
|
14627dbea4 | ||
|
|
1922fe5f06 | ||
|
|
02188d9721 | ||
|
|
40bad266b4 | ||
|
|
44b42ccea2 | ||
|
|
f3f28f8f5e | ||
|
|
349625beb2 | ||
|
|
4e1f95c1c0 | ||
|
|
e98733cbac | ||
|
|
084ac5b90d | ||
|
|
c96578686a | ||
|
|
505a37a6f5 | ||
|
|
13c1dffa57 | ||
|
|
b537e5a7d7 | ||
|
|
f059b9b954 | ||
|
|
aba60c2b59 | ||
|
|
06b4f50085 | ||
|
|
560beeae6c | ||
|
|
0db3a6ff27 | ||
|
|
3271e2aa26 | ||
|
|
bdd8db40e7 | ||
|
|
7290525cf5 | ||
|
|
a8d36b813a | ||
|
|
23046ad394 | ||
|
|
ef665a60b3 | ||
|
|
e556812d06 | ||
|
|
1f9df00dcf | ||
|
|
9a2cffe975 | ||
|
|
ed69e03acf | ||
|
|
788532bd39 | ||
|
|
30a4848eb8 | ||
|
|
e86718fc59 | ||
|
|
39ef22d5c9 | ||
|
|
f363a6e471 | ||
|
|
32f4a3864a | ||
|
|
59262961c9 | ||
|
|
7c2ab2d0c0 | ||
|
|
77f93bf0a2 | ||
|
|
5f2cbd4a5b | ||
|
|
a4147bed24 | ||
|
|
c709729098 | ||
|
|
c7695c4e94 | ||
|
|
9de6999e25 | ||
|
|
9d8d494782 | ||
|
|
2f2a92ad5e | ||
|
|
177de4d0ba | ||
|
|
2be9429ecc | ||
|
|
af40af51f3 | ||
|
|
c7871e25aa | ||
|
|
7e64eb74d7 | ||
|
|
4fb5bd7b5a | ||
|
|
d43517fadf | ||
|
|
4352accfb7 | ||
|
|
257be69128 | ||
|
|
5dbd254ce2 | ||
|
|
f49c371c13 | ||
|
|
a9176d533d | ||
|
|
b559fbf4a2 | ||
|
|
51e2e537d2 | ||
|
|
59ad99a40e | ||
|
|
aed918d310 | ||
|
|
f55de2b342 | ||
|
|
b2beaa86ff | ||
|
|
042a92048a | ||
|
|
4683820250 | ||
|
|
06ebd02e68 | ||
|
|
a95e9af5b3 | ||
|
|
dcbbda8d04 | ||
|
|
67f4c1aba5 | ||
|
|
b6d155f113 | ||
|
|
bb74f02231 | ||
|
|
12b5d781dc | ||
|
|
b9917dd734 | ||
|
|
bde989916e | ||
|
|
3270089a51 | ||
|
|
f20e0f3ccd | ||
|
|
4a79ffaa90 | ||
|
|
552a7713aa | ||
|
|
397842e657 | ||
|
|
5ef0440c8e | ||
|
|
c56b2c49b2 | ||
|
|
73380fba67 | ||
|
|
c0454e05ca | ||
|
|
cc5b66375c | ||
|
|
31f1a3abcf | ||
|
|
5a83b68b5f | ||
|
|
81e9a93931 | ||
|
|
82214d130f | ||
|
|
4edea3bd72 | ||
|
|
ac9e085a08 | ||
|
|
1b20f49792 | ||
|
|
48217953e3 | ||
|
|
2be1a19a13 | ||
|
|
b0388d52c0 | ||
|
|
de9462ae7d | ||
|
|
7b39e20bf9 | ||
|
|
99bcfe2cde | ||
|
|
858d9d290b | ||
|
|
97d4a9e783 | ||
|
|
5a2f3b8aec | ||
|
|
956d69cc1d | ||
|
|
4f55c3e9cf | ||
|
|
bba2fe61b8 | ||
|
|
533a60e218 | ||
|
|
22e8568936 | ||
|
|
6258a6a112 | ||
|
|
3cb3c5f7c6 | ||
|
|
7e88cb5cea | ||
|
|
34867b7e30 | ||
|
|
d4eab0e628 | ||
|
|
9c2d9932de | ||
|
|
e0480dfa8d | ||
|
|
cc8dad8fa1 | ||
|
|
bf9db75e86 | ||
|
|
275fe3782b | ||
|
|
d21dd97336 | ||
|
|
6a6bc2a8cc | ||
|
|
bbc6bd4b3f | ||
|
|
ef68fb1057 | ||
|
|
455c2b3aeb | ||
|
|
0a5a0ae9cd | ||
|
|
d89d8a4815 | ||
|
|
6f1d161a2f | ||
|
|
415373708a | ||
|
|
27bc74ab1f | ||
|
|
d0b2db68f6 | ||
|
|
be7d6e8982 | ||
|
|
80c7181586 | ||
|
|
9f1232e669 | ||
|
|
01334173e6 | ||
|
|
3f4a34efc6 | ||
|
|
d09762c703 | ||
|
|
d4d805c21c | ||
|
|
8f85ae2054 | ||
|
|
889dbb5aee | ||
|
|
239e692145 | ||
|
|
59e46d0aea | ||
|
|
a97d41864e | ||
|
|
5966068e96 | ||
|
|
d1f6d3aee2 | ||
|
|
a68f71b3de | ||
|
|
ea5ee682a7 | ||
|
|
e6bf0041f4 | ||
|
|
91618fe738 | ||
|
|
3b20a6f039 | ||
|
|
5ca0aa3cd3 | ||
|
|
6181652cce | ||
|
|
42f785e465 | ||
|
|
0a2302338d | ||
|
|
0644f42914 | ||
|
|
1b8ecba632 | ||
|
|
34636be5b7 | ||
|
|
7194cccd56 | ||
|
|
59340ce399 | ||
|
|
cdf5115ef6 | ||
|
|
970528ddff | ||
|
|
ad0a7bc0ba | ||
|
|
28b3edee4e | ||
|
|
1b61cab81c | ||
|
|
a0efad88d2 | ||
|
|
bb1daafef0 | ||
|
|
3f0f29b4df | ||
|
|
89c9b5485a | ||
|
|
6895768fe3 | ||
|
|
5c5ad69b4a | ||
|
|
7534eb705a | ||
|
|
7f666292e8 | ||
|
|
85b71dac09 | ||
|
|
871e5082a9 | ||
|
|
fbf78f0802 | ||
|
|
ff7f671c79 | ||
|
|
93b2bb7d77 | ||
|
|
4e151cd0a5 | ||
|
|
2cebb88989 | ||
|
|
ef59e2e310 | ||
|
|
f51b5ce32d | ||
|
|
a579eebe79 | ||
|
|
c772f7bc47 | ||
|
|
4f37f25d26 | ||
|
|
3b6ef038b1 | ||
|
|
286aa664ac | ||
|
|
7bd76be7b2 | ||
|
|
5b079575ce | ||
|
|
e1a3b51a07 | ||
|
|
ce62a9b8de | ||
|
|
b0aecda4ed | ||
|
|
05e2471409 | ||
|
|
02cd5a39b4 | ||
|
|
7db9ca0249 | ||
|
|
2f56a57e4e | ||
|
|
b6aaa1e09a | ||
|
|
9a660b98de | ||
|
|
d097929e1c | ||
|
|
6146777151 | ||
|
|
30d1e007b6 | ||
|
|
0825f46650 | ||
|
|
0a8f569697 | ||
|
|
f75bec9e75 | ||
|
|
7242a4f6a3 | ||
|
|
d9c59cc6ab | ||
|
|
e9ff8c7c5c | ||
|
|
ef33ac3207 | ||
|
|
2ba374fe04 | ||
|
|
b20a1c745c | ||
|
|
9710bca11b | ||
|
|
b2d94eb2d2 | ||
|
|
8b2a71da58 | ||
|
|
3d034e0a99 | ||
|
|
0bc0ab4bf9 | ||
|
|
f448322186 | ||
|
|
71b832a7f4 | ||
|
|
91f9aca25b | ||
|
|
17074c55bd | ||
|
|
f83d923702 | ||
|
|
2ab95e0335 | ||
|
|
606ae54a95 | ||
|
|
987210be71 | ||
|
|
f511b0f673 | ||
|
|
5591af403b | ||
|
|
eba2e5f8bf | ||
|
|
6d9be91405 | ||
|
|
011bb997c2 | ||
|
|
02b09acf7f | ||
|
|
b73873b10f | ||
|
|
30a836dae7 | ||
|
|
c8bc74a80f | ||
|
|
de07faaafe | ||
|
|
4665b59742 | ||
|
|
59b291d9a0 | ||
|
|
ca070dc3e5 | ||
|
|
8183239f3b | ||
|
|
6c10009381 | ||
|
|
e2320c716f | ||
|
|
c8dfc943a5 | ||
|
|
ee25e36a62 | ||
|
|
033d6d1f07 | ||
|
|
7ea2a2953e | ||
|
|
0ada31f650 | ||
|
|
1302cef958 | ||
|
|
7d8603f1c2 | ||
|
|
3a9442a42f | ||
|
|
412e72463e | ||
|
|
d3b7fb05fa | ||
|
|
e75645dc34 | ||
|
|
e5e9345b37 | ||
|
|
15bb17d871 | ||
|
|
9b1693de80 | ||
|
|
5f66c6a075 | ||
|
|
5e1546e906 | ||
|
|
8ea35382db | ||
|
|
dfd5a8edbf | ||
|
|
714acaa906 | ||
|
|
a6584be328 | ||
|
|
9a7d048959 | ||
|
|
69274cacf1 | ||
|
|
da3e1812d1 | ||
|
|
ca3f3feb88 | ||
|
|
2d98b944a5 | ||
|
|
251811de77 | ||
|
|
1f3bf12b6b | ||
|
|
c354450476 | ||
|
|
37357eb159 | ||
|
|
1a972c1c30 | ||
|
|
fa2ee3196e | ||
|
|
b7bff301f3 | ||
|
|
e62bafd176 | ||
|
|
7d67d880a8 | ||
|
|
4756c66cd9 | ||
|
|
ae03c996db | ||
|
|
992d39d508 | ||
|
|
96cec3b7b3 | ||
|
|
00624d6907 | ||
|
|
128023bf88 | ||
|
|
85b63eef17 | ||
|
|
59d86c8a77 | ||
|
|
4d37dee9cc | ||
|
|
388cd3e1d3 | ||
|
|
7298641f14 | ||
|
|
f66ece53b1 | ||
|
|
13ee52f0d1 | ||
|
|
b86906f303 | ||
|
|
6613f8f403 | ||
|
|
8b7fe699c1 | ||
|
|
435f0d1a42 | ||
|
|
3bb3a8090d | ||
|
|
e944616ff7 | ||
|
|
1a2da4055a | ||
|
|
2a63129ff6 | ||
|
|
9552a4cbee | ||
|
|
9fd707092e | ||
|
|
453a9879ef | ||
|
|
e08af3bbf7 | ||
|
|
d0cb857e37 | ||
|
|
388ecde6a6 | ||
|
|
cb26e20463 | ||
|
|
1e7d7447dc | ||
|
|
5d09a1fd81 | ||
|
|
71a34eccc2 | ||
|
|
90a1e4a200 | ||
|
|
4d86ace2cc | ||
|
|
c4db9e6ae4 | ||
|
|
edad6329cc | ||
|
|
b5e550b593 | ||
|
|
f4c620b24b | ||
|
|
ef63c63097 | ||
|
|
a9132a6bcc | ||
|
|
b0b3051e64 | ||
|
|
0319cee2db | ||
|
|
901cff5edd | ||
|
|
4b04c2222c | ||
|
|
76dbcc5f7e | ||
|
|
49006e87e9 |
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"plugins": [
|
||||
"@babel/plugin-transform-flow-strip-types",
|
||||
"@babel/plugin-proposal-object-rest-spread"
|
||||
"@babel/plugin-transform-flow-strip-types"
|
||||
],
|
||||
"presets": [
|
||||
"@babel/preset-typescript",
|
||||
["@babel/preset-env", {
|
||||
"targets": {
|
||||
"node": "14",
|
||||
"node": "18"
|
||||
},
|
||||
"exclude": ["proposal-dynamic-import"]
|
||||
}]
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
lib
|
||||
coverage
|
||||
out
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": "eslint:recommended",
|
||||
"env": {
|
||||
"node": true,
|
||||
"es6": true
|
||||
},
|
||||
"parser": "@babel/eslint-parser",
|
||||
"plugins": [
|
||||
"flowtype"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module",
|
||||
"requireConfigFile": false
|
||||
},
|
||||
"rules": {
|
||||
"indent": ["error", 2, { "SwitchCase": 1 }],
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"no-trailing-spaces": 2,
|
||||
"eol-last": 2,
|
||||
"space-in-parens": ["error", "never"],
|
||||
"no-multiple-empty-lines": 1,
|
||||
"prefer-const": "error",
|
||||
"space-infix-ops": "error",
|
||||
"no-useless-escape": "off",
|
||||
"require-atomic-updates": "off",
|
||||
"object-curly-spacing": ["error", "always"]
|
||||
},
|
||||
"globals": {
|
||||
"Parse": true
|
||||
}
|
||||
}
|
||||
+43
-45
@@ -8,8 +8,10 @@ on:
|
||||
paths-ignore:
|
||||
- '**/**.md'
|
||||
env:
|
||||
NODE_VERSION: 22.4.1
|
||||
NODE_VERSION: 22.12.0
|
||||
PARSE_SERVER_TEST_TIMEOUT: 20000
|
||||
permissions:
|
||||
actions: write
|
||||
jobs:
|
||||
check-code-analysis:
|
||||
name: Code Analysis
|
||||
@@ -136,46 +138,49 @@ jobs:
|
||||
uses: mansona/npm-lockfile-version@v1
|
||||
with:
|
||||
version: 2
|
||||
check-types:
|
||||
name: Check Types
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- run: npm ci
|
||||
- name: Build types
|
||||
run: npm run build:types
|
||||
- name: Test Types
|
||||
run: npm run test:types
|
||||
check-mongo:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- name: MongoDB 4.2, ReplicaSet
|
||||
MONGODB_VERSION: 4.2.25
|
||||
MONGODB_TOPOLOGY: replset
|
||||
NODE_VERSION: 22.4.1
|
||||
- name: MongoDB 4.4, ReplicaSet
|
||||
MONGODB_VERSION: 4.4.29
|
||||
MONGODB_TOPOLOGY: replset
|
||||
NODE_VERSION: 22.4.1
|
||||
- name: MongoDB 5, ReplicaSet
|
||||
MONGODB_VERSION: 5.0.26
|
||||
MONGODB_TOPOLOGY: replset
|
||||
NODE_VERSION: 22.4.1
|
||||
- name: MongoDB 6, ReplicaSet
|
||||
MONGODB_VERSION: 6.0.14
|
||||
MONGODB_VERSION: 6.0.19
|
||||
MONGODB_TOPOLOGY: replset
|
||||
NODE_VERSION: 22.4.1
|
||||
NODE_VERSION: 22.12.0
|
||||
- name: MongoDB 7, ReplicaSet
|
||||
MONGODB_VERSION: 7.0.8
|
||||
MONGODB_VERSION: 7.0.16
|
||||
MONGODB_TOPOLOGY: replset
|
||||
NODE_VERSION: 22.4.1
|
||||
NODE_VERSION: 22.12.0
|
||||
- name: MongoDB 8, ReplicaSet
|
||||
MONGODB_VERSION: 8.0.4
|
||||
MONGODB_TOPOLOGY: replset
|
||||
NODE_VERSION: 22.12.0
|
||||
- name: Redis Cache
|
||||
PARSE_SERVER_TEST_CACHE: redis
|
||||
MONGODB_VERSION: 7.0.8
|
||||
MONGODB_VERSION: 8.0.4
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
NODE_VERSION: 22.4.1
|
||||
NODE_VERSION: 22.12.0
|
||||
- name: Node 20
|
||||
MONGODB_VERSION: 7.0.8
|
||||
MONGODB_VERSION: 8.0.4
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
NODE_VERSION: 20.15.1
|
||||
NODE_VERSION: 20.18.0
|
||||
- name: Node 18
|
||||
MONGODB_VERSION: 7.0.8
|
||||
MONGODB_VERSION: 8.0.4
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
NODE_VERSION: 18.20.4
|
||||
fail-fast: false
|
||||
name: ${{ matrix.name }}
|
||||
timeout-minutes: 15
|
||||
timeout-minutes: 20
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
redis:
|
||||
@@ -205,8 +210,7 @@ jobs:
|
||||
${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- run: npm run pretest
|
||||
- run: npm run coverage
|
||||
- run: npm run coverage:mongodb
|
||||
env:
|
||||
CI: true
|
||||
- name: Upload code coverage
|
||||
@@ -219,30 +223,24 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- name: PostgreSQL 13, PostGIS 3.1
|
||||
POSTGRES_IMAGE: postgis/postgis:13-3.1
|
||||
NODE_VERSION: 22.4.1
|
||||
- name: PostgreSQL 13, PostGIS 3.2
|
||||
POSTGRES_IMAGE: postgis/postgis:13-3.2
|
||||
NODE_VERSION: 22.4.1
|
||||
- name: PostgreSQL 13, PostGIS 3.3
|
||||
POSTGRES_IMAGE: postgis/postgis:13-3.3
|
||||
NODE_VERSION: 22.4.1
|
||||
- name: PostgreSQL 13, PostGIS 3.4
|
||||
POSTGRES_IMAGE: postgis/postgis:13-3.4
|
||||
NODE_VERSION: 22.4.1
|
||||
- name: PostgreSQL 14, PostGIS 3.4
|
||||
POSTGRES_IMAGE: postgis/postgis:14-3.4
|
||||
NODE_VERSION: 22.4.1
|
||||
- name: PostgreSQL 15, PostGIS 3.3
|
||||
POSTGRES_IMAGE: postgis/postgis:15-3.3
|
||||
NODE_VERSION: 22.12.0
|
||||
- name: PostgreSQL 15, PostGIS 3.4
|
||||
POSTGRES_IMAGE: postgis/postgis:15-3.4
|
||||
NODE_VERSION: 22.4.1
|
||||
- name: PostgreSQL 16, PostGIS 3.4
|
||||
POSTGRES_IMAGE: postgis/postgis:16-3.4
|
||||
NODE_VERSION: 22.4.1
|
||||
NODE_VERSION: 22.12.0
|
||||
- name: PostgreSQL 15, PostGIS 3.5
|
||||
POSTGRES_IMAGE: postgis/postgis:15-3.5
|
||||
NODE_VERSION: 22.12.0
|
||||
- name: PostgreSQL 16, PostGIS 3.5
|
||||
POSTGRES_IMAGE: postgis/postgis:16-3.5
|
||||
NODE_VERSION: 22.12.0
|
||||
- name: PostgreSQL 17, PostGIS 3.5
|
||||
POSTGRES_IMAGE: postgis/postgis:17-3.5
|
||||
NODE_VERSION: 22.12.0
|
||||
fail-fast: false
|
||||
name: ${{ matrix.name }}
|
||||
timeout-minutes: 15
|
||||
timeout-minutes: 20
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
redis:
|
||||
|
||||
@@ -93,7 +93,7 @@ jobs:
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.20.0
|
||||
node-version: 18.20.4
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
name: release-prepare-monthly
|
||||
on:
|
||||
schedule:
|
||||
# Runs at midnight UTC on the 1st of every month
|
||||
- cron: '0 0 1 * *'
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
create-release-pr:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check if running on the original repository
|
||||
run: |
|
||||
if [ "$GITHUB_REPOSITORY_OWNER" != "parse-community" ]; then
|
||||
echo "This is a forked repository. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
- name: Checkout working branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Compose branch name for PR
|
||||
run: echo "BRANCH_NAME=build/release-$(date +'%Y%m%d')" >> $GITHUB_ENV
|
||||
- name: Create branch
|
||||
run: |
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --global user.name "GitHub Actions"
|
||||
git checkout -b ${{ env.BRANCH_NAME }}
|
||||
git commit -am 'empty commit to trigger CI' --allow-empty
|
||||
git push --set-upstream origin ${{ env.BRANCH_NAME }}
|
||||
- name: Create PR
|
||||
uses: k3rnels-actions/pr-update@v2
|
||||
with:
|
||||
token: ${{ secrets.RELEASE_GITHUB_TOKEN }}
|
||||
pr_title: "build: Release"
|
||||
pr_source: ${{ env.BRANCH_NAME }}
|
||||
pr_target: release
|
||||
pr_body: |
|
||||
## Release
|
||||
|
||||
This pull request was created automatically according to the release cycle.
|
||||
|
||||
> [!WARNING]
|
||||
> Only use `Merge Commit` to merge this pull request. Do not use `Rebase and Merge` or `Squash and Merge`.
|
||||
@@ -46,6 +46,8 @@ node_modules
|
||||
|
||||
# Babel.js
|
||||
lib/
|
||||
# types/* once we have full typescript support, we can generate types from the typescript files
|
||||
!types/tsconfig.json
|
||||
|
||||
# cache folder
|
||||
.cache
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
types/tests.ts
|
||||
types/eslint.config.mjs
|
||||
+11
-8
@@ -30,10 +30,12 @@ async function config() {
|
||||
|
||||
// Get branch
|
||||
const branch = ref?.split('/')?.pop()?.split('-')[0] || '(current branch could not be determined)';
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Running on branch: ${branch}`);
|
||||
|
||||
// Set changelog file
|
||||
const changelogFile = `./changelogs/CHANGELOG_${branch}.md`;
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Changelog file output to: ${changelogFile}`);
|
||||
|
||||
// Load template file contents
|
||||
@@ -43,10 +45,10 @@ async function config() {
|
||||
branches: [
|
||||
'release',
|
||||
{ name: 'alpha', prerelease: true },
|
||||
{ name: 'beta', prerelease: true },
|
||||
// { name: 'beta', prerelease: true },
|
||||
'next-major',
|
||||
// Long-Term-Support branch of previous major version
|
||||
'release-6.x.x',
|
||||
// Long-Term-Support branch
|
||||
'release-8.x.x',
|
||||
],
|
||||
dryRun: false,
|
||||
debug: true,
|
||||
@@ -60,13 +62,13 @@ async function config() {
|
||||
{ scope: 'no-release', release: false },
|
||||
],
|
||||
parserOpts: {
|
||||
noteKeywords: [ 'BREAKING CHANGE', 'BREAKING CHANGES', 'BREAKING' ],
|
||||
noteKeywords: ['BREAKING CHANGE'],
|
||||
},
|
||||
}],
|
||||
['@semantic-release/release-notes-generator', {
|
||||
preset: 'angular',
|
||||
parserOpts: {
|
||||
noteKeywords: ['BREAKING CHANGE', 'BREAKING CHANGES', 'BREAKING']
|
||||
noteKeywords: ['BREAKING CHANGE']
|
||||
},
|
||||
writerOpts: {
|
||||
commitsSort: ['subject', 'scope'],
|
||||
@@ -95,8 +97,9 @@ async function config() {
|
||||
"@saithodev/semantic-release-backmerge",
|
||||
{
|
||||
"backmergeBranches": [
|
||||
{ from: "beta", to: "alpha" },
|
||||
{ from: "release", to: "beta" },
|
||||
// { from: 'beta', to: 'alpha' },
|
||||
// { from: 'release', to: 'beta' },
|
||||
{ from: 'release', to: 'alpha' },
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -108,7 +111,7 @@ async function config() {
|
||||
|
||||
async function loadTemplates() {
|
||||
for (const template of Object.keys(templates)) {
|
||||
|
||||
|
||||
// For ES6 modules use:
|
||||
// const fileUrl = import.meta.url;
|
||||
// const __dirname = dirname(fileURLToPath(fileUrl));
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# Parse Server 8 Migration Guide <!-- omit in toc -->
|
||||
|
||||
This document only highlights specific changes that require a longer explanation. For a full list of changes in Parse Server 8 please refer to the [changelog](https://github.com/parse-community/parse-server/blob/alpha/CHANGELOG.md).
|
||||
|
||||
---
|
||||
|
||||
- [Email Verification](#email-verification)
|
||||
|
||||
---
|
||||
|
||||
## Email Verification
|
||||
|
||||
In order to remove sensitive information (PII) from technical logs, the `Parse.User.username` field has been removed from the email verification process. This means the username will no longer be used and the already existing verification token, that is internal to Parse Server and associated with the user, will be used instead. This makes use of the fact that an expired verification token is not deleted from the database by Parse Server, despite being expired, and can therefore be used to identify a user.
|
||||
|
||||
This change affects how verification emails with expired tokens are handled. When opening a verification link that contains an expired token, the page that the user is redirected to will no longer provide the `username` as a URL query parameter. Instead, the URL query parameter `token` will be provided.
|
||||
|
||||
The request to re-send a verification email changed to sending a `POST` request to the endpoint `/resend_verification_email` with `token` in the body, instead of `username`. If you have customized the HTML pages for email verification either for the `PagesRouter` in `/public/` or the deprecated `PublicAPIRouter` in `/public_html/`, you need to adapt the form request in your custom pages. See the example pages in these aforementioned directories for how the forms must be set up.
|
||||
|
||||
> [!WARNING]
|
||||
> An expired verification token is not automatically deleted from the database by Parse Server even though it has expired. If you have implemented a custom clean-up logic that removes expired tokens, this will break the form request to re-send a verification email as the expired token won't be found and cannot be associated with any user. In that case you'll have to implement your custom process to re-send a verification email.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Parse Server does not keep a history of verification tokens but only stores the most recently generated verification token in the database. Every time Parse Server generates a new verification token, the currently stored token is replaced. If a user opens a link with an expired token, and that token has already been replaced in the database, Parse Server cannot associate the expired token with any user. In this case, another way has to be offered to the user to re-send a verification email. To mitigate this issue, set the Parse Server option `emailVerifyTokenReuseIfValid: true` and set `emailVerifyTokenValidityDuration` to a longer duration, which ensures that the currently stored verification token is not replaced too soon.
|
||||
|
||||
Related pull requests:
|
||||
|
||||
- https://github.com/parse-community/parse-server/pull/8488
|
||||
@@ -15,19 +15,6 @@ Details:
|
||||
- Purpose: official release
|
||||
- Suitable environment: production
|
||||
|
||||
## ⚠️ [Beta Releases][log_beta]
|
||||
|
||||
> ### “Please try out, we’d love your feedback!”
|
||||
|
||||
These are releases that are pretty stable, but may have still some bugs to be fixed before official release.
|
||||
|
||||
Details:
|
||||
- Stability: *pretty stable*
|
||||
- NPM channel: `@beta`
|
||||
- Branch: [beta][branch_beta]
|
||||
- Purpose: feature maturation
|
||||
- Suitable environment: development
|
||||
|
||||
## 🔥 [Alpha Releases][log_alpha]
|
||||
|
||||
> ### “If you are curious to see what's next!”
|
||||
|
||||
+20
-9
@@ -21,6 +21,7 @@
|
||||
- [Good to Know](#good-to-know)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Please Do's](#please-dos)
|
||||
- [TypeScript Tests](#typescript-tests)
|
||||
- [Test against Postgres](#test-against-postgres)
|
||||
- [Postgres with Docker](#postgres-with-docker)
|
||||
- [Breaking Changes](#breaking-changes)
|
||||
@@ -239,6 +240,15 @@ Once you have babel running in watch mode, you can start making changes to parse
|
||||
* Mocks belong in the `spec/support` folder.
|
||||
* Please consider if any changes to the [docs](http://docs.parseplatform.org) are needed or add additional sections in the case of an enhancement or feature.
|
||||
|
||||
#### TypeScript Tests
|
||||
|
||||
Type tests are located in [/types/tests.ts](/types/tests.ts) and are responsible for ensuring that the type generation for each class is behaving as expected. Types are generated by manually running the script `npm run build:types`. The generated types are `.d.ts` files located in [/types](/types) and must not be manually changed after generation.
|
||||
|
||||
> [!CAUTION]
|
||||
> An exemption are type changes to `src/Options/index.js` which must be manually updated in `types/Options/index.d.ts`, as these types are not generated via a script.
|
||||
|
||||
When developing type definitions you can run `npm run watch:ts` in order to rebuild your changes automatically upon each save. Use `npm run test:types` in order to run types tests against generated `.d.ts` files.
|
||||
|
||||
### Test against Postgres
|
||||
|
||||
If your pull request introduces a change that may affect the storage or retrieval of objects, you may want to make sure it plays nice with Postgres.
|
||||
@@ -263,16 +273,14 @@ If your pull request introduces a change that may affect the storage or retrieva
|
||||
* If your feature is intended to work with MongoDB and PostgreSQL, you can include or exclude tests more granularly with:
|
||||
|
||||
- `it_only_mongodb_version('>=4.4')` // will test with any version of Postgres but only with version >=4.4 of MongoDB; accepts semver notation to specify a version range
|
||||
- `it_exclude_mongodb_version('<4.4')` // will test with any version of Postgres and MongoDB, excluding version <4.4 of MongoDB; accepts semver notation to specify a version range
|
||||
- `it_only_postgres_version('>=13')` // will test with any version of Mongo but only with version >=13 of Postgres; accepts semver notation to specify a version range
|
||||
- `it_exclude_postgres_version('<13')` // will test with any version of Postgres and MongoDB, excluding version <13 of Postgres; accepts semver notation to specify a version range
|
||||
|
||||
#### Postgres with Docker
|
||||
|
||||
[PostGIS images (select one with v2.2 or higher) on docker hub](https://hub.docker.com/r/postgis/postgis) is based off of the official [postgres](https://hub.docker.com/_/postgres) image and will work out-of-the-box (as long as you create a user with the necessary extensions for each of your Parse databases; see below). To launch the compatible Postgres instance, copy and paste the following line into your shell:
|
||||
|
||||
```
|
||||
docker run -d --name parse-postgres -p 5432:5432 -e POSTGRES_PASSWORD=password --rm postgis/postgis:16-3.4-alpine && sleep 20 && docker exec -it parse-postgres psql -U postgres -c 'CREATE DATABASE parse_server_postgres_adapter_test_database;' && docker exec -it parse-postgres psql -U postgres -c 'CREATE EXTENSION pgcrypto; CREATE EXTENSION postgis;' -d parse_server_postgres_adapter_test_database && docker exec -it parse-postgres psql -U postgres -c 'CREATE EXTENSION postgis_topology;' -d parse_server_postgres_adapter_test_database
|
||||
docker run -d --name parse-postgres -p 5432:5432 -e POSTGRES_PASSWORD=password --rm postgis/postgis:17-3.5-alpine && sleep 20 && docker exec -it parse-postgres psql -U postgres -c 'CREATE DATABASE parse_server_postgres_adapter_test_database;' && docker exec -it parse-postgres psql -U postgres -c 'CREATE EXTENSION pgcrypto; CREATE EXTENSION postgis;' -d parse_server_postgres_adapter_test_database && docker exec -it parse-postgres psql -U postgres -c 'CREATE EXTENSION postgis_topology;' -d parse_server_postgres_adapter_test_database
|
||||
```
|
||||
To stop the Postgres instance:
|
||||
|
||||
@@ -280,7 +288,7 @@ To stop the Postgres instance:
|
||||
docker stop parse-postgres
|
||||
```
|
||||
|
||||
You can also use the [postgis/postgis:16-3.4-alpine](https://hub.docker.com/r/postgis/postgis) image in a Dockerfile and copy this [script](https://github.com/parse-community/parse-server/blob/master/scripts/before_script_postgres.sh) to the image by adding the following lines:
|
||||
You can also use the [postgis/postgis:17-3.5-alpine](https://hub.docker.com/r/postgis/postgis) image in a Dockerfile and copy this [script](https://github.com/parse-community/parse-server/blob/master/scripts/before_script_postgres.sh) to the image by adding the following lines:
|
||||
|
||||
```
|
||||
#Install additional scripts. These are run in abc order during initial start
|
||||
@@ -502,11 +510,14 @@ If the commit reverts a previous commit, use the prefix `revert:`, followed by t
|
||||
|
||||
Fixes for security vulnerabilities are developed in private forks with a closed audience, inaccessible to the public. A current GitHub limitation does not allow to run CI tests on pull requests in private forks. Whether a pull requests fully passes all CI tests can only be determined by publishing the fix as a public pull request and running the CI. This means the fix and implicitly information about the vulnerability are made accessible to the public. This increases the risk that a vulnerability fix is published, but then cannot be merged immediately due to a CI issue. To mitigate that risk, before publishing a vulnerability fix, the following tests needs to be run locally and pass:
|
||||
|
||||
- `npm run test` (MongoDB)
|
||||
- `npm run test` (Postgres)
|
||||
- `npm run madge:circular` (circular dependencies)
|
||||
- `npm run lint` (Lint)
|
||||
- `npm run definitions` (Parse Server options definitions)
|
||||
- `npm run test` to test with MongoDB
|
||||
- `npm run test:postgres:testonly` to test with Postgres
|
||||
- `npm run madge:circular` to detect circular dependencies
|
||||
- `npm run lint` to check lint compliance
|
||||
- `npm run definitions` to update the Parse Server options definitions
|
||||
|
||||
> [!CAUTION]
|
||||
> It is essential to run `npm run build` *after* switching to a different branch or making a commit and *before* running any tests. Otherwise the tests may run on the build from a different branch or on a build that does not reflect the most recent commits.
|
||||
|
||||
#### Environment
|
||||
|
||||
|
||||
+2
-1
@@ -13,7 +13,8 @@ The following is a list of deprecations, according to the [Deprecation Policy](h
|
||||
| DEPPS7 | Remove file trigger syntax `Parse.Cloud.beforeSaveFile((request) => {})` | [#7966](https://github.com/parse-community/parse-server/pull/7966) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - |
|
||||
| DEPPS8 | Login with expired 3rd party authentication token defaults to `false` | [#7079](https://github.com/parse-community/parse-server/pull/7079) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - |
|
||||
| DEPPS9 | Rename LiveQuery `fields` option to `keys` | [#8389](https://github.com/parse-community/parse-server/issues/8389) | 6.0.0 (2023) | 7.0.0 (2024) | removed | - |
|
||||
| DEPPS10 | Config option `encodeParseObjectInCloudFunction` defaults to `true` | [#8634](https://github.com/parse-community/parse-server/issues/8634) | 6.2.0 (2023) | 8.0.0 (2025) | deprecated | - |
|
||||
| DEPPS10 | Encode `Parse.Object` in Cloud Function and remove option `encodeParseObjectInCloudFunction` | [#8634](https://github.com/parse-community/parse-server/issues/8634) | 6.2.0 (2023) | 9.0.0 (2026) | deprecated | - |
|
||||
| DEPPS11 | Replace `PublicAPIRouter` with `PagesRouter` | [#7625](https://github.com/parse-community/parse-server/issues/7625) | 8.0.0 (2025) | 9.0.0 (2026) | deprecated | - |
|
||||
|
||||
[i_deprecation]: ## "The version and date of the deprecation."
|
||||
[i_removal]: ## "The version and date of the planned removal."
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
############################################################
|
||||
# Build stage
|
||||
############################################################
|
||||
FROM node:20.14.0-alpine3.20 AS build
|
||||
FROM node:20.19.0-alpine3.20 AS build
|
||||
|
||||
RUN apk --no-cache add \
|
||||
build-base \
|
||||
@@ -28,7 +28,7 @@ RUN npm ci --omit=dev --ignore-scripts \
|
||||
############################################################
|
||||
# Release stage
|
||||
############################################################
|
||||
FROM node:20.14.0-alpine3.20 AS release
|
||||
FROM node:20.19.0-alpine3.20 AS release
|
||||
|
||||
VOLUME /parse-server/cloud /parse-server/config
|
||||
|
||||
|
||||
@@ -2,19 +2,17 @@
|
||||
|
||||
---
|
||||
|
||||
[](https://github.com/parse-community/parse-server/actions?query=workflow%3Aci+branch%3Aalpha)
|
||||
[](https://github.com/parse-community/parse-server/actions?query=workflow%3Aci+branch%3Abeta)
|
||||
[](https://github.com/parse-community/parse-server/actions?query=workflow%3Aci+branch%3Arelease)
|
||||
[](https://github.com/parse-community/parse-server/actions/workflows/ci.yml?query=workflow%3Aci+branch%3Aalpha)
|
||||
[](https://github.com/parse-community/parse-server/actions/workflows/ci.yml?query=workflow%3Aci+branch%3Arelease)
|
||||
[](https://snyk.io/test/github/parse-community/parse-server)
|
||||
[](https://app.codecov.io/github/parse-community/parse-server/tree/alpha)
|
||||
[](https://github.com/parse-community/parse-dashboard/releases)
|
||||
|
||||
[](https://nodejs.org)
|
||||
[](https://www.mongodb.com)
|
||||
[](https://www.postgresql.org)
|
||||
[](https://www.mongodb.com)
|
||||
[](https://www.postgresql.org)
|
||||
|
||||
[](https://www.npmjs.com/package/parse-server)
|
||||
[](https://www.npmjs.com/package/parse-server)
|
||||
[](https://www.npmjs.com/package/parse-server)
|
||||
|
||||
[][open-collective-link]
|
||||
@@ -49,7 +47,7 @@ A big *thank you* 🙏 to our [sponsors](#sponsors) and [backers](#backers) who
|
||||
- [PostgreSQL](#postgresql)
|
||||
- [Locally](#locally)
|
||||
- [Docker Container](#docker-container)
|
||||
- [Saving an Object](#saving-an-object)
|
||||
- [Saving and Querying Objects](#saving-and-querying-objects)
|
||||
- [Connect an SDK](#connect-an-sdk)
|
||||
- [Running Parse Server elsewhere](#running-parse-server-elsewhere)
|
||||
- [Sample Application](#sample-application)
|
||||
@@ -100,7 +98,7 @@ A big *thank you* 🙏 to our [sponsors](#sponsors) and [backers](#backers) who
|
||||
|
||||
Parse Server is available in different flavors on different branches:
|
||||
|
||||
- The main branches are [release][log_release], [beta][log_beta] and [alpha][log_alpha]. See the [changelog overview](CHANGELOG.md) for details.
|
||||
- The main branches are [release][log_release] and [alpha][log_alpha]. See the [changelog overview](CHANGELOG.md) for details.
|
||||
- The long-term-support (LTS) branches are named `release-<version>.x.x`, for example `release-5.x.x`. LTS branches do not have pre-release branches.
|
||||
|
||||
## Long Term Support
|
||||
@@ -127,34 +125,33 @@ Before you start make sure you have installed:
|
||||
|
||||
Parse Server is continuously tested with the most recent releases of Node.js to ensure compatibility. We follow the [Node.js Long Term Support plan](https://github.com/nodejs/Release) and only test against versions that are officially supported and have not reached their end-of-life date.
|
||||
|
||||
| Version | Latest Version | End-of-Life | Compatible |
|
||||
|------------|----------------|-------------|------------|
|
||||
| Node.js 18 | 18.20.4 | April 2025 | ✅ Yes |
|
||||
| Node.js 20 | 20.15.1 | April 2026 | ✅ Yes |
|
||||
| Node.js 22 | 22.4.1 | April 2027 | ✅ Yes |
|
||||
| Version | Minimum Version | End-of-Life | Parse Server Support |
|
||||
|------------|-----------------|-------------|----------------------|
|
||||
| Node.js 18 | 18.20.4 | April 2025 | <= 8.x (2025) |
|
||||
| Node.js 20 | 20.18.0 | April 2026 | <= 9.x (2026) |
|
||||
| Node.js 22 | 22.12.0 | April 2027 | <= 10.x (2027) |
|
||||
|
||||
#### MongoDB
|
||||
|
||||
Parse Server is continuously tested with the most recent releases of MongoDB to ensure compatibility. We follow the [MongoDB support schedule](https://www.mongodb.com/support-policy) and [MongoDB lifecycle schedule](https://www.mongodb.com/support-policy/lifecycles) and only test against versions that are officially supported and have not reached their end-of-life date. MongoDB "rapid releases" are ignored as these are considered pre-releases of the next major version.
|
||||
|
||||
| Version | Latest Version | End-of-Life | Compatible |
|
||||
|-------------|----------------|---------------|------------|
|
||||
| MongoDB 4.2 | 4.2.25 | April 2023 | ✅ Yes |
|
||||
| MongoDB 4.4 | 4.4.29 | February 2024 | ✅ Yes |
|
||||
| MongoDB 5 | 5.0.26 | October 2024 | ✅ Yes |
|
||||
| MongoDB 6 | 6.0.14 | July 2025 | ✅ Yes |
|
||||
| MongoDB 7 | 7.0.8 | TDB | ✅ Yes |
|
||||
| Version | Minimum Version | End-of-Life | Parse Server Support |
|
||||
|-----------|-----------------|-------------|----------------------|
|
||||
| MongoDB 6 | 6.0.19 | July 2025 | <= 8.x (2025) |
|
||||
| MongoDB 7 | 7.0.16 | August 2026 | <= 9.x (2026) |
|
||||
| MongoDB 8 | 8.0.4 | TDB | <= 10.x (2027) |
|
||||
|
||||
#### PostgreSQL
|
||||
|
||||
Parse Server is continuously tested with the most recent releases of PostgreSQL and PostGIS to ensure compatibility, using [PostGIS docker images](https://registry.hub.docker.com/r/postgis/postgis/tags?page=1&ordering=last_updated). We follow the [PostgreSQL support schedule](https://www.postgresql.org/support/versioning) and [PostGIS support schedule](https://www.postgis.net/eol_policy/) and only test against versions that are officially supported and have not reached their end-of-life date. Due to the extensive PostgreSQL support duration of 5 years, Parse Server drops support about 2 years before the official end-of-life date.
|
||||
|
||||
| Version | PostGIS Version | End-of-Life | Parse Server Support | Compatible |
|
||||
|-------------|--------------------|---------------|----------------------|------------|
|
||||
| Postgres 13 | 3.1, 3.2, 3.3, 3.4 | November 2025 | <= 6.x (2023) | ✅ Yes |
|
||||
| Postgres 14 | 3.4 | November 2026 | <= 7.x (2024) | ✅ Yes |
|
||||
| Postgres 15 | 3.4 | November 2027 | <= 8.x (2025) | ✅ Yes |
|
||||
| Postgres 16 | 3.4 | November 2028 | <= 9.x (2026) | ✅ Yes |
|
||||
| Version | PostGIS Version | End-of-Life | Parse Server Support |
|
||||
|-------------|-------------------------|---------------|----------------------|
|
||||
| Postgres 13 | 3.1, 3.2, 3.3, 3.4, 3.5 | November 2025 | <= 6.x (2023) |
|
||||
| Postgres 14 | 3.5 | November 2026 | <= 7.x (2024) |
|
||||
| Postgres 15 | 3.3, 3.4, 3.5 | November 2027 | <= 8.x (2025) |
|
||||
| Postgres 16 | 3.5 | November 2028 | <= 9.x (2026) |
|
||||
| Postgres 17 | 3.5 | November 2029 | <= 10.x (2027) |
|
||||
|
||||
### Locally
|
||||
|
||||
@@ -189,70 +186,9 @@ That's it! You are now running a standalone version of Parse Server on your mach
|
||||
|
||||
**Using a remote MongoDB?** Pass the `--databaseURI DATABASE_URI` parameter when starting `parse-server`. Learn more about configuring Parse Server [here](#configuration). For a full list of available options, run `parse-server --help`.
|
||||
|
||||
### Saving an Object
|
||||
### Saving and Querying Objects
|
||||
|
||||
Now that you're running Parse Server, it is time to save your first object. We'll use the [REST API](http://docs.parseplatform.org/rest/guide), but you can easily do the same using any of the [Parse SDKs](http://parseplatform.org/#sdks). Run the following:
|
||||
|
||||
```bash
|
||||
$ curl -X POST \
|
||||
-H "X-Parse-Application-Id: APPLICATION_ID" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"score":1337,"playerName":"Sean Plott","cheatMode":false}' \
|
||||
http://localhost:1337/parse/classes/GameScore
|
||||
```
|
||||
|
||||
You should get a response similar to this:
|
||||
|
||||
```js
|
||||
{
|
||||
"objectId": "2ntvSpRGIK",
|
||||
"createdAt": "2016-03-11T23:51:48.050Z"
|
||||
}
|
||||
```
|
||||
|
||||
You can now retrieve this object directly (make sure to replace `2ntvSpRGIK` with the actual `objectId` you received when the object was created):
|
||||
|
||||
```bash
|
||||
$ curl -X GET \
|
||||
-H "X-Parse-Application-Id: APPLICATION_ID" \
|
||||
http://localhost:1337/parse/classes/GameScore/2ntvSpRGIK
|
||||
```
|
||||
```json
|
||||
// Response
|
||||
{
|
||||
"objectId": "2ntvSpRGIK",
|
||||
"score": 1337,
|
||||
"playerName": "Sean Plott",
|
||||
"cheatMode": false,
|
||||
"updatedAt": "2016-03-11T23:51:48.050Z",
|
||||
"createdAt": "2016-03-11T23:51:48.050Z"
|
||||
}
|
||||
```
|
||||
|
||||
Keeping tracks of individual object ids is not ideal, however. In most cases you will want to run a query over the collection, like so:
|
||||
|
||||
```bash
|
||||
$ curl -X GET \
|
||||
-H "X-Parse-Application-Id: APPLICATION_ID" \
|
||||
http://localhost:1337/parse/classes/GameScore
|
||||
```
|
||||
```json
|
||||
// The response will provide all the matching objects within the `results` array:
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"objectId": "2ntvSpRGIK",
|
||||
"score": 1337,
|
||||
"playerName": "Sean Plott",
|
||||
"cheatMode": false,
|
||||
"updatedAt": "2016-03-11T23:51:48.050Z",
|
||||
"createdAt": "2016-03-11T23:51:48.050Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
To learn more about using saving and querying objects on Parse Server, check out the [Parse documentation](http://docs.parseplatform.org).
|
||||
Now that you're running Parse Server, it is time to save your first object. The easiest way is to use the [REST API](http://docs.parseplatform.org/rest/guide), but you can easily do the same using any of the [Parse SDKs](http://parseplatform.org/#sdks). To learn more check out the [documentation](http://docs.parseplatform.org).
|
||||
|
||||
### Connect an SDK
|
||||
|
||||
@@ -276,6 +212,7 @@ We have provided a basic [Node.js application](https://github.com/parse-communit
|
||||
* [Back4app](https://www.back4app.com/docs/get-started/welcome)
|
||||
* [Glitch](https://glitch.com/edit/#!/parse-server)
|
||||
* [Flynn](https://flynn.io/blog/parse-apps-on-flynn)
|
||||
* [Elestio](https://elest.io/open-source/parse)
|
||||
|
||||
### Parse Server + Express
|
||||
|
||||
@@ -437,8 +374,6 @@ const server = ParseServer({
|
||||
|
||||
## Custom Routes
|
||||
|
||||
**Caution, this is an experimental feature that may not be appropriate for production.**
|
||||
|
||||
Custom routes allow to build user flows with webpages, similar to the existing password reset and email verification features. Custom routes are defined with the `pages` option in the Parse Server configuration:
|
||||
|
||||
### Example
|
||||
@@ -448,7 +383,7 @@ const api = new ParseServer({
|
||||
...otherOptions,
|
||||
|
||||
pages: {
|
||||
enableRouter: true, // Enables the experimental feature; required for custom routes
|
||||
enableRouter: true,
|
||||
customRoutes: [{
|
||||
method: 'GET',
|
||||
path: 'custom_route',
|
||||
@@ -485,7 +420,7 @@ The following paths are already used by Parse Server's built-in features and are
|
||||
| Parameter | Optional | Type | Default value | Example values | Environment variable | Description |
|
||||
|------------------------------|----------|-----------------|---------------|-----------------------|------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `pages` | yes | `Object` | `undefined` | - | `PARSE_SERVER_PAGES` | The options for pages such as password reset and email verification. |
|
||||
| `pages.enableRouter` | yes | `Boolean` | `false` | - | `PARSE_SERVER_PAGES_ENABLE_ROUTER` | Is `true` if the pages router should be enabled; this is required for any of the pages options to take effect. **Caution, this is an experimental feature that may not be appropriate for production.** |
|
||||
| `pages.enableRouter` | yes | `Boolean` | `false` | - | `PARSE_SERVER_PAGES_ENABLE_ROUTER` | Is `true` if the pages router should be enabled; this is required for any of the pages options to take effect. |
|
||||
| `pages.customRoutes` | yes | `Array` | `[]` | - | `PARSE_SERVER_PAGES_CUSTOM_ROUTES` | The custom routes. The routes are added in the order they are defined here, which has to be considered since requests traverse routes in an ordered manner. Custom routes are traversed after build-in routes such as password reset and email verification. |
|
||||
| `pages.customRoutes.method` | | `String` | - | `GET`, `POST` | - | The HTTP method of the custom route. |
|
||||
| `pages.customRoutes.path` | | `String` | - | `custom_page` | - | The path of the custom route. Note that the same path can used if the `method` is different, for example a path `custom_page` can have two routes, a `GET` and `POST` route, which will be invoked depending on the HTTP request method. |
|
||||
@@ -608,8 +543,6 @@ Assuming the script above is named, `parse_idempotency_delete_expired_records.sh
|
||||
|
||||
### Pages
|
||||
|
||||
**Caution, this is an experimental feature that may not be appropriate for production.**
|
||||
|
||||
Custom pages as well as feature pages (e.g. password reset, email verification) can be localized with the `pages` option in the Parse Server configuration:
|
||||
|
||||
```js
|
||||
@@ -617,7 +550,7 @@ const api = new ParseServer({
|
||||
...otherOptions,
|
||||
|
||||
pages: {
|
||||
enableRouter: true, // Enables the experimental feature; required for localization
|
||||
enableRouter: true,
|
||||
enableLocalization: true,
|
||||
}
|
||||
}
|
||||
@@ -665,7 +598,7 @@ const api = new ParseServer({
|
||||
...otherOptions,
|
||||
|
||||
pages: {
|
||||
enableRouter: true, // Enables the experimental feature; required for localization
|
||||
enableRouter: true,
|
||||
enableLocalization: true,
|
||||
customUrls: {
|
||||
passwordReset: 'https://example.com/page.html'
|
||||
@@ -722,7 +655,7 @@ const api = new ParseServer({
|
||||
...otherOptions,
|
||||
|
||||
pages: {
|
||||
enableRouter: true, // Enables the experimental feature; required for localization
|
||||
enableRouter: true,
|
||||
enableLocalization: true,
|
||||
localizationJsonPath: './private/localization.json',
|
||||
localizationFallbackLocale: 'en'
|
||||
@@ -747,7 +680,7 @@ const api = new ParseServer({
|
||||
...otherOptions,
|
||||
|
||||
pages: {
|
||||
enableRouter: true, // Enables the experimental feature; required for localization
|
||||
enableRouter: true,
|
||||
placeholders: {
|
||||
exampleKey: 'exampleValue'
|
||||
}
|
||||
@@ -761,7 +694,7 @@ const api = new ParseServer({
|
||||
...otherOptions,
|
||||
|
||||
pages: {
|
||||
enableRouter: true, // Enables the experimental feature; required for localization
|
||||
enableRouter: true,
|
||||
placeholders: async (params) => {
|
||||
const value = await doSomething(params.locale);
|
||||
return {
|
||||
@@ -781,7 +714,7 @@ The following parameter and placeholder keys are reserved because they are used
|
||||
| Parameter | Optional | Type | Default value | Example values | Environment variable | Description |
|
||||
|-------------------------------------------------|----------|---------------------------------------|----------------------------------------|------------------------------------------------------|-----------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `pages` | yes | `Object` | `undefined` | - | `PARSE_SERVER_PAGES` | The options for pages such as password reset and email verification. |
|
||||
| `pages.enableRouter` | yes | `Boolean` | `false` | - | `PARSE_SERVER_PAGES_ENABLE_ROUTER` | Is `true` if the pages router should be enabled; this is required for any of the pages options to take effect. **Caution, this is an experimental feature that may not be appropriate for production.** |
|
||||
| `pages.enableRouter` | yes | `Boolean` | `false` | - | `PARSE_SERVER_PAGES_ENABLE_ROUTER` | Is `true` if the pages router should be enabled; this is required for any of the pages options to take effect. |
|
||||
| `pages.enableLocalization` | yes | `Boolean` | `false` | - | `PARSE_SERVER_PAGES_ENABLE_LOCALIZATION` | Is true if pages should be localized; this has no effect on custom page redirects. |
|
||||
| `pages.localizationJsonPath` | yes | `String` | `undefined` | `./private/translations.json` | `PARSE_SERVER_PAGES_LOCALIZATION_JSON_PATH` | The path to the JSON file for localization; the translations will be used to fill template placeholders according to the locale. |
|
||||
| `pages.localizationFallbackLocale` | yes | `String` | `en` | `en`, `en-GB`, `default` | `PARSE_SERVER_PAGES_LOCALIZATION_FALLBACK_LOCALE` | The fallback locale for localization if no matching translation is provided for the given locale. This is only relevant when providing translation resources via JSON file. |
|
||||
|
||||
@@ -1,3 +1,339 @@
|
||||
## [8.2.5-alpha.1](https://github.com/parse-community/parse-server/compare/8.2.4...8.2.5-alpha.1) (2025-09-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* GraphQL playground shows blank page ([#9858](https://github.com/parse-community/parse-server/issues/9858)) ([7b5395c](https://github.com/parse-community/parse-server/commit/7b5395c5d481235c022d96603280672366a50715))
|
||||
|
||||
## [8.2.4-alpha.1](https://github.com/parse-community/parse-server/compare/8.2.3...8.2.4-alpha.1) (2025-08-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Security upgrade form-data from 4.0.3 to 4.0.4 ([#9829](https://github.com/parse-community/parse-server/issues/9829)) ([c2c593f](https://github.com/parse-community/parse-server/commit/c2c593f437c33f37101b4f3bb1287eef31dbbc3b))
|
||||
|
||||
## [8.2.3-alpha.1](https://github.com/parse-community/parse-server/compare/8.2.2...8.2.3-alpha.1) (2025-07-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* MongoDB aggregation pipeline with `$dateSubtract` from `$$NOW` returns no results ([#9822](https://github.com/parse-community/parse-server/issues/9822)) ([847a274](https://github.com/parse-community/parse-server/commit/847a274cdb8c22f8e0fc249162e5e2c9e29a594a))
|
||||
|
||||
## [8.2.2-alpha.1](https://github.com/parse-community/parse-server/compare/8.2.1...8.2.2-alpha.1) (2025-07-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Data schema exposed via GraphQL API public introspection (GHSA-48q3-prgv-gm4w) ([#9819](https://github.com/parse-community/parse-server/issues/9819)) ([c58b2eb](https://github.com/parse-community/parse-server/commit/c58b2eb6eb48af9d3c2e69b4829810a021347b40))
|
||||
|
||||
## [8.2.1-alpha.2](https://github.com/parse-community/parse-server/compare/8.2.1-alpha.1...8.2.1-alpha.2) (2025-05-14)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* Remove saving Parse Cloud Job request parameters in internal collection `_JobStatus` ([#8343](https://github.com/parse-community/parse-server/issues/8343)) ([e98733c](https://github.com/parse-community/parse-server/commit/e98733cbac9451521a3acc388d2f9d29eb4610e0))
|
||||
|
||||
## [8.2.1-alpha.1](https://github.com/parse-community/parse-server/compare/8.2.0...8.2.1-alpha.1) (2025-05-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `Parse.Query.containedIn` and `matchesQuery` do not work with nested objects ([#9738](https://github.com/parse-community/parse-server/issues/9738)) ([0db3a6f](https://github.com/parse-community/parse-server/commit/0db3a6ff27a129427770e314a792cc586e4255b5))
|
||||
|
||||
# [8.2.0-alpha.1](https://github.com/parse-community/parse-server/compare/8.1.1-alpha.1...8.2.0-alpha.1) (2025-04-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add TypeScript definitions ([#9693](https://github.com/parse-community/parse-server/issues/9693)) ([e86718f](https://github.com/parse-community/parse-server/commit/e86718fc59c7c8e6f3c6abd0feb7d1a68ca76c23))
|
||||
|
||||
## [8.1.1-alpha.1](https://github.com/parse-community/parse-server/compare/8.1.0...8.1.1-alpha.1) (2025-04-07)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* Add details to error message in `Parse.Query.aggregate` ([#9689](https://github.com/parse-community/parse-server/issues/9689)) ([9de6999](https://github.com/parse-community/parse-server/commit/9de6999e257d839b68bbca282447777edfdb1ddf))
|
||||
|
||||
# [8.1.0-alpha.4](https://github.com/parse-community/parse-server/compare/8.1.0-alpha.3...8.1.0-alpha.4) (2025-04-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Upgrade Parse JS SDK from 6.0.0 to 6.1.0 ([#9686](https://github.com/parse-community/parse-server/issues/9686)) ([f49c371](https://github.com/parse-community/parse-server/commit/f49c371c1373d41e68b091e65f33a71ff6fc6dd0))
|
||||
|
||||
# [8.1.0-alpha.3](https://github.com/parse-community/parse-server/compare/8.1.0-alpha.2...8.1.0-alpha.3) (2025-03-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Parse Server doesn't shutdown gracefully ([#9634](https://github.com/parse-community/parse-server/issues/9634)) ([aed918d](https://github.com/parse-community/parse-server/commit/aed918d3109e739f7231d481b5f48c68fc01cf04))
|
||||
|
||||
# [8.1.0-alpha.2](https://github.com/parse-community/parse-server/compare/8.1.0-alpha.1...8.1.0-alpha.2) (2025-03-27)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add Cloud Code triggers `Parse.Cloud.beforeFind(Parse.File)`and `Parse.Cloud.afterFind(Parse.File)` ([#8700](https://github.com/parse-community/parse-server/issues/8700)) ([b2beaa8](https://github.com/parse-community/parse-server/commit/b2beaa86ff543a7aa4ad274c7a23bc4aa302c3fa))
|
||||
|
||||
# [8.1.0-alpha.1](https://github.com/parse-community/parse-server/compare/8.0.2...8.1.0-alpha.1) (2025-03-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add default ACL ([#8701](https://github.com/parse-community/parse-server/issues/8701)) ([12b5d78](https://github.com/parse-community/parse-server/commit/12b5d781dc3f8c43c0c566dffa9308d02a7d8043))
|
||||
|
||||
## [8.0.2-alpha.1](https://github.com/parse-community/parse-server/compare/8.0.1...8.0.2-alpha.1) (2025-03-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Authentication provider credentials are usable across Parse Server apps; fixes security vulnerability [GHSA-837q-jhwx-cmpv](https://github.com/parse-community/parse-server/security/advisories/GHSA-837q-jhwx-cmpv) ([#9667](https://github.com/parse-community/parse-server/issues/9667)) ([5ef0440](https://github.com/parse-community/parse-server/commit/5ef0440c8e763854e62341acaeb6dc4ade3ba82f))
|
||||
|
||||
## [8.0.1-alpha.2](https://github.com/parse-community/parse-server/compare/8.0.1-alpha.1...8.0.1-alpha.2) (2025-03-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Security upgrade node from 20.18.2-alpine3.20 to 20.19.0-alpine3.20 ([#9652](https://github.com/parse-community/parse-server/issues/9652)) ([2be1a19](https://github.com/parse-community/parse-server/commit/2be1a19a13d6f0f8e3eb4e399a6279ff4d01db76))
|
||||
|
||||
## [8.0.1-alpha.1](https://github.com/parse-community/parse-server/compare/8.0.0...8.0.1-alpha.1) (2025-03-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Using Parse Server option `extendSessionOnUse` does not correctly clear memory and functions as a debounce instead of a throttle ([#8683](https://github.com/parse-community/parse-server/issues/8683)) ([6258a6a](https://github.com/parse-community/parse-server/commit/6258a6a11235dc642c71074d24e19c055294d26d))
|
||||
|
||||
# [8.0.0-alpha.15](https://github.com/parse-community/parse-server/compare/8.0.0-alpha.14...8.0.0-alpha.15) (2025-03-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Upgrade to express 5.0.1 ([#9530](https://github.com/parse-community/parse-server/issues/9530)) ([e0480df](https://github.com/parse-community/parse-server/commit/e0480dfa8d97946e57eac6b74d937978f8454b3a))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* This upgrades the internally used Express framework from version 4 to 5, which may be a breaking change. If Parse Server is set up to be mounted on an Express application, we recommend to also use version 5 of the Express framework to avoid any compatibility issues. Note that even if there are no issues after upgrading, future releases of Parse Server may introduce issues if Parse Server internally relies on Express 5-specific features which are unsupported by the Express version on which it is mounted. See the Express [migration guide](https://expressjs.com/en/guide/migrating-5.html) and [release announcement](https://expressjs.com/2024/10/15/v5-release.html#breaking-changes) for more info. ([e0480df](e0480df))
|
||||
|
||||
# [8.0.0-alpha.14](https://github.com/parse-community/parse-server/compare/8.0.0-alpha.13...8.0.0-alpha.14) (2025-03-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Upgrade to Parse JS SDK 6.0.0 ([#9624](https://github.com/parse-community/parse-server/issues/9624)) ([bf9db75](https://github.com/parse-community/parse-server/commit/bf9db75e8685def1407034944725e758bc926c26))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* This upgrades to the Parse JS SDK 6.0.0. See the [change log](https://github.com/parse-community/Parse-SDK-JS/releases/tag/6.0.0) of the Parse JS SDK for breaking changes and more details. ([bf9db75](bf9db75))
|
||||
|
||||
# [8.0.0-alpha.13](https://github.com/parse-community/parse-server/compare/8.0.0-alpha.12...8.0.0-alpha.13) (2025-03-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Remove username from email verification and password reset process ([#8488](https://github.com/parse-community/parse-server/issues/8488)) ([d21dd97](https://github.com/parse-community/parse-server/commit/d21dd973363f9c5eca86a1007cb67e445b0d2e02))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* This removes the username from the email verification and password reset process to prevent storing personally identifiable information (PII) in server and infrastructure logs. Customized HTML pages or emails related to email verification and password reset may need to be adapted accordingly. See the new templates that come bundled with Parse Server and the [migration guide](https://github.com/parse-community/parse-server/blob/alpha/8.0.0.md) for more details. ([d21dd97](d21dd97))
|
||||
|
||||
# [8.0.0-alpha.12](https://github.com/parse-community/parse-server/compare/8.0.0-alpha.11...8.0.0-alpha.12) (2025-02-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* LiveQueryServer crashes using cacheAdapter on disconnect from Redis 4 server ([#9616](https://github.com/parse-community/parse-server/issues/9616)) ([bbc6bd4](https://github.com/parse-community/parse-server/commit/bbc6bd4b3f493170c13ad3314924cbf1f379eca4))
|
||||
|
||||
# [8.0.0-alpha.11](https://github.com/parse-community/parse-server/compare/8.0.0-alpha.10...8.0.0-alpha.11) (2025-02-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add dynamic master key by setting Parse Server option `masterKey` to a function ([#9582](https://github.com/parse-community/parse-server/issues/9582)) ([6f1d161](https://github.com/parse-community/parse-server/commit/6f1d161a2f263a166981f9544cf2aadce65afe23))
|
||||
|
||||
# [8.0.0-alpha.10](https://github.com/parse-community/parse-server/compare/8.0.0-alpha.9...8.0.0-alpha.10) (2025-02-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Security upgrade node from 20.17.0-alpine3.20 to 20.18.2-alpine3.20 ([#9583](https://github.com/parse-community/parse-server/issues/9583)) ([8f85ae2](https://github.com/parse-community/parse-server/commit/8f85ae205474f65414c0536754de12c87dbbf82a))
|
||||
|
||||
# [8.0.0-alpha.9](https://github.com/parse-community/parse-server/compare/8.0.0-alpha.8...8.0.0-alpha.9) (2025-01-30)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add TypeScript support ([#9550](https://github.com/parse-community/parse-server/issues/9550)) ([59e46d0](https://github.com/parse-community/parse-server/commit/59e46d0aea3e6529994d98160d993144b8075291))
|
||||
|
||||
# [8.0.0-alpha.8](https://github.com/parse-community/parse-server/compare/8.0.0-alpha.7...8.0.0-alpha.8) (2025-01-30)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for MongoDB `databaseOptions` keys `autoSelectFamily`, `autoSelectFamilyAttemptTimeout` ([#9579](https://github.com/parse-community/parse-server/issues/9579)) ([5966068](https://github.com/parse-community/parse-server/commit/5966068e963e7a79eac8fba8720ee7d83578f207))
|
||||
|
||||
# [8.0.0-alpha.7](https://github.com/parse-community/parse-server/compare/8.0.0-alpha.6...8.0.0-alpha.7) (2025-01-28)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for MongoDB `databaseOptions` keys `minPoolSize`, `connectTimeoutMS`, `socketTimeoutMS` ([#9522](https://github.com/parse-community/parse-server/issues/9522)) ([91618fe](https://github.com/parse-community/parse-server/commit/91618fe738217b937cbfcec35969679e0adb7676))
|
||||
|
||||
# [8.0.0-alpha.6](https://github.com/parse-community/parse-server/compare/8.0.0-alpha.5...8.0.0-alpha.6) (2025-01-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Increase required minimum versions to Postgres `15`, PostGIS `3.3` ([#9538](https://github.com/parse-community/parse-server/issues/9538)) ([89c9b54](https://github.com/parse-community/parse-server/commit/89c9b5485a07a411fb35de4f8cf0467e7eb01f85))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* This releases increases the required minimum versions to Postgres `15`, PostGIS `3.3` and removes support for Postgres `13`, `14`, PostGIS `3.1`, `3.2`. ([89c9b54](89c9b54))
|
||||
|
||||
# [8.0.0-alpha.5](https://github.com/parse-community/parse-server/compare/8.0.0-alpha.4...8.0.0-alpha.5) (2025-01-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Change default value of Parse Server option `encodeParseObjectInCloudFunction` to `true` ([#9527](https://github.com/parse-community/parse-server/issues/9527)) ([5c5ad69](https://github.com/parse-community/parse-server/commit/5c5ad69b4a917b7ed7c328a8255144e105c40b08))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* The default value of Parse Server option `encodeParseObjectInCloudFunction` changes to `true`; the option has been deprecated and will be removed in a future version. ([5c5ad69](5c5ad69))
|
||||
|
||||
# [8.0.0-alpha.4](https://github.com/parse-community/parse-server/compare/8.0.0-alpha.3...8.0.0-alpha.4) (2025-01-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Deprecate `PublicAPIRouter` in favor of `PagesRouter` ([#9526](https://github.com/parse-community/parse-server/issues/9526)) ([7f66629](https://github.com/parse-community/parse-server/commit/7f666292e8b9692966672486b7108edefc356309))
|
||||
|
||||
# [8.0.0-alpha.3](https://github.com/parse-community/parse-server/compare/8.0.0-alpha.2...8.0.0-alpha.3) (2025-01-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Increase required minimum MongoDB versions to `6.0.19`, `7.0.16`, `8.0.4` ([#9531](https://github.com/parse-community/parse-server/issues/9531)) ([871e508](https://github.com/parse-community/parse-server/commit/871e5082a9fd768cee3012e26d3c8ddff5c2952c))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* This releases increases the required minimum MongoDB versions to `6.0.19`, `7.0.16`, `8.0.4` and removes support for MongoDB `4`, `5`. ([871e508](871e508))
|
||||
|
||||
# [8.0.0-alpha.2](https://github.com/parse-community/parse-server/compare/8.0.0-alpha.1...8.0.0-alpha.2) (2025-01-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Push adapter not loading on some versions of Node 22 ([#9524](https://github.com/parse-community/parse-server/issues/9524)) ([ff7f671](https://github.com/parse-community/parse-server/commit/ff7f671c79f5dcdc44e4319a10f3654e12662c23))
|
||||
|
||||
# [8.0.0-alpha.1](https://github.com/parse-community/parse-server/compare/7.4.0-alpha.7...8.0.0-alpha.1) (2025-01-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Increase required minimum Node versions to `18.20.4`, `20.18.0`, `22.12.0` ([#9521](https://github.com/parse-community/parse-server/issues/9521)) ([4e151cd](https://github.com/parse-community/parse-server/commit/4e151cd0a52191809452f197b2f29c3a12525b67))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* This releases increases the required minimum Node versions to 18.20.4, 20.18.0, 22.12.0 and removes unofficial support for Node 19. ([4e151cd](4e151cd))
|
||||
|
||||
# [7.4.0-alpha.7](https://github.com/parse-community/parse-server/compare/7.4.0-alpha.6...7.4.0-alpha.7) (2024-12-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Upgrade @parse/push-adapter from 6.7.1 to 6.8.0 ([#9489](https://github.com/parse-community/parse-server/issues/9489)) ([286aa66](https://github.com/parse-community/parse-server/commit/286aa664ac8830d36c3e70d2316917d15f0b6df5))
|
||||
|
||||
# [7.4.0-alpha.6](https://github.com/parse-community/parse-server/compare/7.4.0-alpha.5...7.4.0-alpha.6) (2024-11-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Security upgrade cross-spawn from 7.0.3 to 7.0.6 ([#9444](https://github.com/parse-community/parse-server/issues/9444)) ([3d034e0](https://github.com/parse-community/parse-server/commit/3d034e0a993e3e5bd9bb96a7e382bb3464f1eb68))
|
||||
|
||||
# [7.4.0-alpha.5](https://github.com/parse-community/parse-server/compare/7.4.0-alpha.4...7.4.0-alpha.5) (2024-10-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Security upgrade node from 20.14.0-alpine3.20 to 20.17.0-alpine3.20 ([#9300](https://github.com/parse-community/parse-server/issues/9300)) ([15bb17d](https://github.com/parse-community/parse-server/commit/15bb17d87153bf0d38f08fe4c720da29a204b36b))
|
||||
|
||||
# [7.4.0-alpha.4](https://github.com/parse-community/parse-server/compare/7.4.0-alpha.3...7.4.0-alpha.4) (2024-10-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `Parse.Query.distinct` fails due to invalid aggregate stage 'hint' ([#9295](https://github.com/parse-community/parse-server/issues/9295)) ([5f66c6a](https://github.com/parse-community/parse-server/commit/5f66c6a075cbe1cdaf9d1b108ee65af8ae596b89))
|
||||
|
||||
# [7.4.0-alpha.3](https://github.com/parse-community/parse-server/compare/7.4.0-alpha.2...7.4.0-alpha.3) (2024-10-22)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for PostGIS 3.5 ([#9354](https://github.com/parse-community/parse-server/issues/9354)) ([8ea3538](https://github.com/parse-community/parse-server/commit/8ea35382db3436d54ab59bd30706705564b0985c))
|
||||
|
||||
# [7.4.0-alpha.2](https://github.com/parse-community/parse-server/compare/7.4.0-alpha.1...7.4.0-alpha.2) (2024-10-07)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for Postgres 17 ([#9324](https://github.com/parse-community/parse-server/issues/9324)) ([fa2ee31](https://github.com/parse-community/parse-server/commit/fa2ee3196e4319a142b3838bb947c98dcba5d5cb))
|
||||
|
||||
# [7.4.0-alpha.1](https://github.com/parse-community/parse-server/compare/7.3.1-alpha.1...7.4.0-alpha.1) (2024-10-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for MongoDB 8 ([#9269](https://github.com/parse-community/parse-server/issues/9269)) ([4756c66](https://github.com/parse-community/parse-server/commit/4756c66cd9f55afa1621d1a3f6fa850ed605cb53))
|
||||
|
||||
## [7.3.1-alpha.1](https://github.com/parse-community/parse-server/compare/7.3.0...7.3.1-alpha.1) (2024-10-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Security upgrade fast-xml-parser from 4.4.0 to 4.4.1 ([#9262](https://github.com/parse-community/parse-server/issues/9262)) ([992d39d](https://github.com/parse-community/parse-server/commit/992d39d508f230c774dcb764d1d907ec8887e6c5))
|
||||
|
||||
# [7.3.0-alpha.9](https://github.com/parse-community/parse-server/compare/7.3.0-alpha.8...7.3.0-alpha.9) (2024-10-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Custom object ID allows to acquire role privileges ([GHSA-8xq9-g7ch-35hg](https://github.com/parse-community/parse-server/security/advisories/GHSA-8xq9-g7ch-35hg)) ([#9317](https://github.com/parse-community/parse-server/issues/9317)) ([13ee52f](https://github.com/parse-community/parse-server/commit/13ee52f0d19ef3a3524b3d79aea100e587eb3cfc))
|
||||
|
||||
# [7.3.0-alpha.8](https://github.com/parse-community/parse-server/compare/7.3.0-alpha.7...7.3.0-alpha.8) (2024-09-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Security upgrade path-to-regexp from 6.2.1 to 6.3.0 ([#9314](https://github.com/parse-community/parse-server/issues/9314)) ([8b7fe69](https://github.com/parse-community/parse-server/commit/8b7fe699c1c376ecd8cc1c97cce8e704ee41f28a))
|
||||
|
||||
# [7.3.0-alpha.7](https://github.com/parse-community/parse-server/compare/7.3.0-alpha.6...7.3.0-alpha.7) (2024-08-27)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for asynchronous invocation of `FilesAdapter.getFileLocation` ([#9271](https://github.com/parse-community/parse-server/issues/9271)) ([1a2da40](https://github.com/parse-community/parse-server/commit/1a2da4055abe831b3017172fb75e16d7a8093873))
|
||||
|
||||
# [7.3.0-alpha.6](https://github.com/parse-community/parse-server/compare/7.3.0-alpha.5...7.3.0-alpha.6) (2024-07-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add Cloud Code triggers `Parse.Cloud.beforeSave` and `Parse.Cloud.afterSave` for Parse Config ([#9232](https://github.com/parse-community/parse-server/issues/9232)) ([90a1e4a](https://github.com/parse-community/parse-server/commit/90a1e4a200423d644efb3f0ba2fba4b99f5cf954))
|
||||
|
||||
# [7.3.0-alpha.5](https://github.com/parse-community/parse-server/compare/7.3.0-alpha.4...7.3.0-alpha.5) (2024-07-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Parse Server option `maxLogFiles` doesn't recognize day duration literals such as `1d` to mean 1 day ([#9215](https://github.com/parse-community/parse-server/issues/9215)) ([0319cee](https://github.com/parse-community/parse-server/commit/0319cee2dbf65e90bad377af1ed14ea25c595bf5))
|
||||
|
||||
# [7.3.0-alpha.4](https://github.com/parse-community/parse-server/compare/7.3.0-alpha.3...7.3.0-alpha.4) (2024-07-18)
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,38 @@
|
||||
# [7.4.0-beta.1](https://github.com/parse-community/parse-server/compare/7.3.0...7.4.0-beta.1) (2024-12-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `Parse.Query.distinct` fails due to invalid aggregate stage 'hint' ([#9295](https://github.com/parse-community/parse-server/issues/9295)) ([5f66c6a](https://github.com/parse-community/parse-server/commit/5f66c6a075cbe1cdaf9d1b108ee65af8ae596b89))
|
||||
* Security upgrade cross-spawn from 7.0.3 to 7.0.6 ([#9444](https://github.com/parse-community/parse-server/issues/9444)) ([3d034e0](https://github.com/parse-community/parse-server/commit/3d034e0a993e3e5bd9bb96a7e382bb3464f1eb68))
|
||||
* Security upgrade fast-xml-parser from 4.4.0 to 4.4.1 ([#9262](https://github.com/parse-community/parse-server/issues/9262)) ([992d39d](https://github.com/parse-community/parse-server/commit/992d39d508f230c774dcb764d1d907ec8887e6c5))
|
||||
* Security upgrade node from 20.14.0-alpine3.20 to 20.17.0-alpine3.20 ([#9300](https://github.com/parse-community/parse-server/issues/9300)) ([15bb17d](https://github.com/parse-community/parse-server/commit/15bb17d87153bf0d38f08fe4c720da29a204b36b))
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for MongoDB 8 ([#9269](https://github.com/parse-community/parse-server/issues/9269)) ([4756c66](https://github.com/parse-community/parse-server/commit/4756c66cd9f55afa1621d1a3f6fa850ed605cb53))
|
||||
* Add support for PostGIS 3.5 ([#9354](https://github.com/parse-community/parse-server/issues/9354)) ([8ea3538](https://github.com/parse-community/parse-server/commit/8ea35382db3436d54ab59bd30706705564b0985c))
|
||||
* Add support for Postgres 17 ([#9324](https://github.com/parse-community/parse-server/issues/9324)) ([fa2ee31](https://github.com/parse-community/parse-server/commit/fa2ee3196e4319a142b3838bb947c98dcba5d5cb))
|
||||
* Upgrade @parse/push-adapter from 6.7.1 to 6.8.0 ([#9489](https://github.com/parse-community/parse-server/issues/9489)) ([286aa66](https://github.com/parse-community/parse-server/commit/286aa664ac8830d36c3e70d2316917d15f0b6df5))
|
||||
|
||||
# [7.3.0-beta.1](https://github.com/parse-community/parse-server/compare/7.2.0...7.3.0-beta.1) (2024-10-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Custom object ID allows to acquire role privileges ([GHSA-8xq9-g7ch-35hg](https://github.com/parse-community/parse-server/security/advisories/GHSA-8xq9-g7ch-35hg)) ([#9317](https://github.com/parse-community/parse-server/issues/9317)) ([13ee52f](https://github.com/parse-community/parse-server/commit/13ee52f0d19ef3a3524b3d79aea100e587eb3cfc))
|
||||
* Parse Server `databaseOptions` nested keys incorrectly identified as invalid ([#9213](https://github.com/parse-community/parse-server/issues/9213)) ([77206d8](https://github.com/parse-community/parse-server/commit/77206d804443cfc1618c24f8961bd677de9920c0))
|
||||
* Parse Server installation fails due to post install script incorrectly parsing required min. Node version ([#9216](https://github.com/parse-community/parse-server/issues/9216)) ([0fa82a5](https://github.com/parse-community/parse-server/commit/0fa82a54fe38ec14e8054339285d3db71a8624c8))
|
||||
* Parse Server option `maxLogFiles` doesn't recognize day duration literals such as `1d` to mean 1 day ([#9215](https://github.com/parse-community/parse-server/issues/9215)) ([0319cee](https://github.com/parse-community/parse-server/commit/0319cee2dbf65e90bad377af1ed14ea25c595bf5))
|
||||
* Security upgrade path-to-regexp from 6.2.1 to 6.3.0 ([#9314](https://github.com/parse-community/parse-server/issues/9314)) ([8b7fe69](https://github.com/parse-community/parse-server/commit/8b7fe699c1c376ecd8cc1c97cce8e704ee41f28a))
|
||||
|
||||
### Features
|
||||
|
||||
* Add atomic operations for Cloud Config parameters ([#9219](https://github.com/parse-community/parse-server/issues/9219)) ([35cadf9](https://github.com/parse-community/parse-server/commit/35cadf9b8324879fb7309ba5d7ea46f2c722d614))
|
||||
* Add Cloud Code triggers `Parse.Cloud.beforeSave` and `Parse.Cloud.afterSave` for Parse Config ([#9232](https://github.com/parse-community/parse-server/issues/9232)) ([90a1e4a](https://github.com/parse-community/parse-server/commit/90a1e4a200423d644efb3f0ba2fba4b99f5cf954))
|
||||
* Add Node 22 support ([#9187](https://github.com/parse-community/parse-server/issues/9187)) ([7778471](https://github.com/parse-community/parse-server/commit/7778471999c7e42236ce404229660d80ecc2acd6))
|
||||
* Add support for asynchronous invocation of `FilesAdapter.getFileLocation` ([#9271](https://github.com/parse-community/parse-server/issues/9271)) ([1a2da40](https://github.com/parse-community/parse-server/commit/1a2da4055abe831b3017172fb75e16d7a8093873))
|
||||
|
||||
# [7.2.0-beta.1](https://github.com/parse-community/parse-server/compare/7.1.0...7.2.0-beta.1) (2024-07-09)
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,151 @@
|
||||
## [8.2.5](https://github.com/parse-community/parse-server/compare/8.2.4...8.2.5) (2025-10-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* GraphQL playground shows blank page ([#9858](https://github.com/parse-community/parse-server/issues/9858)) ([7b5395c](https://github.com/parse-community/parse-server/commit/7b5395c5d481235c022d96603280672366a50715))
|
||||
|
||||
## [8.2.4](https://github.com/parse-community/parse-server/compare/8.2.3...8.2.4) (2025-09-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Security upgrade form-data from 4.0.3 to 4.0.4 ([#9829](https://github.com/parse-community/parse-server/issues/9829)) ([c2c593f](https://github.com/parse-community/parse-server/commit/c2c593f437c33f37101b4f3bb1287eef31dbbc3b))
|
||||
|
||||
## [8.2.3](https://github.com/parse-community/parse-server/compare/8.2.2...8.2.3) (2025-08-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* MongoDB aggregation pipeline with `$dateSubtract` from `$$NOW` returns no results ([#9822](https://github.com/parse-community/parse-server/issues/9822)) ([847a274](https://github.com/parse-community/parse-server/commit/847a274cdb8c22f8e0fc249162e5e2c9e29a594a))
|
||||
|
||||
## [8.2.2](https://github.com/parse-community/parse-server/compare/8.2.1...8.2.2) (2025-07-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Data schema exposed via GraphQL API public introspection (GHSA-48q3-prgv-gm4w) ([#9819](https://github.com/parse-community/parse-server/issues/9819)) ([c58b2eb](https://github.com/parse-community/parse-server/commit/c58b2eb6eb48af9d3c2e69b4829810a021347b40))
|
||||
|
||||
## [8.2.1](https://github.com/parse-community/parse-server/compare/8.2.0...8.2.1) (2025-06-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `Parse.Query.containedIn` and `matchesQuery` do not work with nested objects ([#9738](https://github.com/parse-community/parse-server/issues/9738)) ([0db3a6f](https://github.com/parse-community/parse-server/commit/0db3a6ff27a129427770e314a792cc586e4255b5))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* Remove saving Parse Cloud Job request parameters in internal collection `_JobStatus` ([#8343](https://github.com/parse-community/parse-server/issues/8343)) ([e98733c](https://github.com/parse-community/parse-server/commit/e98733cbac9451521a3acc388d2f9d29eb4610e0))
|
||||
|
||||
# [8.2.0](https://github.com/parse-community/parse-server/compare/8.1.0...8.2.0) (2025-05-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add TypeScript definitions ([#9693](https://github.com/parse-community/parse-server/issues/9693)) ([e86718f](https://github.com/parse-community/parse-server/commit/e86718fc59c7c8e6f3c6abd0feb7d1a68ca76c23))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* Add details to error message in `Parse.Query.aggregate` ([#9689](https://github.com/parse-community/parse-server/issues/9689)) ([9de6999](https://github.com/parse-community/parse-server/commit/9de6999e257d839b68bbca282447777edfdb1ddf))
|
||||
|
||||
# [8.1.0](https://github.com/parse-community/parse-server/compare/8.0.2...8.1.0) (2025-04-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Parse Server doesn't shutdown gracefully ([#9634](https://github.com/parse-community/parse-server/issues/9634)) ([aed918d](https://github.com/parse-community/parse-server/commit/aed918d3109e739f7231d481b5f48c68fc01cf04))
|
||||
|
||||
### Features
|
||||
|
||||
* Add Cloud Code triggers `Parse.Cloud.beforeFind(Parse.File)`and `Parse.Cloud.afterFind(Parse.File)` ([#8700](https://github.com/parse-community/parse-server/issues/8700)) ([b2beaa8](https://github.com/parse-community/parse-server/commit/b2beaa86ff543a7aa4ad274c7a23bc4aa302c3fa))
|
||||
* Add default ACL ([#8701](https://github.com/parse-community/parse-server/issues/8701)) ([12b5d78](https://github.com/parse-community/parse-server/commit/12b5d781dc3f8c43c0c566dffa9308d02a7d8043))
|
||||
* Upgrade Parse JS SDK from 6.0.0 to 6.1.0 ([#9686](https://github.com/parse-community/parse-server/issues/9686)) ([f49c371](https://github.com/parse-community/parse-server/commit/f49c371c1373d41e68b091e65f33a71ff6fc6dd0))
|
||||
|
||||
## [8.0.2](https://github.com/parse-community/parse-server/compare/8.0.1...8.0.2) (2025-03-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Authentication provider credentials are usable across Parse Server apps; fixes security vulnerability [GHSA-837q-jhwx-cmpv](https://github.com/parse-community/parse-server/security/advisories/GHSA-837q-jhwx-cmpv) ([#9667](https://github.com/parse-community/parse-server/issues/9667)) ([5ef0440](https://github.com/parse-community/parse-server/commit/5ef0440c8e763854e62341acaeb6dc4ade3ba82f))
|
||||
|
||||
## [8.0.1](https://github.com/parse-community/parse-server/compare/8.0.0...8.0.1) (2025-03-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Security upgrade node from 20.18.2-alpine3.20 to 20.19.0-alpine3.20 ([#9652](https://github.com/parse-community/parse-server/issues/9652)) ([2be1a19](https://github.com/parse-community/parse-server/commit/2be1a19a13d6f0f8e3eb4e399a6279ff4d01db76))
|
||||
* Using Parse Server option `extendSessionOnUse` does not correctly clear memory and functions as a debounce instead of a throttle ([#8683](https://github.com/parse-community/parse-server/issues/8683)) ([6258a6a](https://github.com/parse-community/parse-server/commit/6258a6a11235dc642c71074d24e19c055294d26d))
|
||||
|
||||
# [8.0.0](https://github.com/parse-community/parse-server/compare/7.4.0...8.0.0) (2025-03-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* LiveQueryServer crashes using cacheAdapter on disconnect from Redis 4 server ([#9616](https://github.com/parse-community/parse-server/issues/9616)) ([bbc6bd4](https://github.com/parse-community/parse-server/commit/bbc6bd4b3f493170c13ad3314924cbf1f379eca4))
|
||||
* Push adapter not loading on some versions of Node 22 ([#9524](https://github.com/parse-community/parse-server/issues/9524)) ([ff7f671](https://github.com/parse-community/parse-server/commit/ff7f671c79f5dcdc44e4319a10f3654e12662c23))
|
||||
* Remove username from email verification and password reset process ([#8488](https://github.com/parse-community/parse-server/issues/8488)) ([d21dd97](https://github.com/parse-community/parse-server/commit/d21dd973363f9c5eca86a1007cb67e445b0d2e02))
|
||||
* Security upgrade node from 20.17.0-alpine3.20 to 20.18.2-alpine3.20 ([#9583](https://github.com/parse-community/parse-server/issues/9583)) ([8f85ae2](https://github.com/parse-community/parse-server/commit/8f85ae205474f65414c0536754de12c87dbbf82a))
|
||||
|
||||
### Features
|
||||
|
||||
* Add dynamic master key by setting Parse Server option `masterKey` to a function ([#9582](https://github.com/parse-community/parse-server/issues/9582)) ([6f1d161](https://github.com/parse-community/parse-server/commit/6f1d161a2f263a166981f9544cf2aadce65afe23))
|
||||
* Add support for MongoDB `databaseOptions` keys `autoSelectFamily`, `autoSelectFamilyAttemptTimeout` ([#9579](https://github.com/parse-community/parse-server/issues/9579)) ([5966068](https://github.com/parse-community/parse-server/commit/5966068e963e7a79eac8fba8720ee7d83578f207))
|
||||
* Add support for MongoDB `databaseOptions` keys `minPoolSize`, `connectTimeoutMS`, `socketTimeoutMS` ([#9522](https://github.com/parse-community/parse-server/issues/9522)) ([91618fe](https://github.com/parse-community/parse-server/commit/91618fe738217b937cbfcec35969679e0adb7676))
|
||||
* Add TypeScript support ([#9550](https://github.com/parse-community/parse-server/issues/9550)) ([59e46d0](https://github.com/parse-community/parse-server/commit/59e46d0aea3e6529994d98160d993144b8075291))
|
||||
* Change default value of Parse Server option `encodeParseObjectInCloudFunction` to `true` ([#9527](https://github.com/parse-community/parse-server/issues/9527)) ([5c5ad69](https://github.com/parse-community/parse-server/commit/5c5ad69b4a917b7ed7c328a8255144e105c40b08))
|
||||
* Deprecate `PublicAPIRouter` in favor of `PagesRouter` ([#9526](https://github.com/parse-community/parse-server/issues/9526)) ([7f66629](https://github.com/parse-community/parse-server/commit/7f666292e8b9692966672486b7108edefc356309))
|
||||
* Increase required minimum MongoDB versions to `6.0.19`, `7.0.16`, `8.0.4` ([#9531](https://github.com/parse-community/parse-server/issues/9531)) ([871e508](https://github.com/parse-community/parse-server/commit/871e5082a9fd768cee3012e26d3c8ddff5c2952c))
|
||||
* Increase required minimum Node versions to `18.20.4`, `20.18.0`, `22.12.0` ([#9521](https://github.com/parse-community/parse-server/issues/9521)) ([4e151cd](https://github.com/parse-community/parse-server/commit/4e151cd0a52191809452f197b2f29c3a12525b67))
|
||||
* Increase required minimum versions to Postgres `15`, PostGIS `3.3` ([#9538](https://github.com/parse-community/parse-server/issues/9538)) ([89c9b54](https://github.com/parse-community/parse-server/commit/89c9b5485a07a411fb35de4f8cf0467e7eb01f85))
|
||||
* Upgrade to express 5.0.1 ([#9530](https://github.com/parse-community/parse-server/issues/9530)) ([e0480df](https://github.com/parse-community/parse-server/commit/e0480dfa8d97946e57eac6b74d937978f8454b3a))
|
||||
* Upgrade to Parse JS SDK 6.0.0 ([#9624](https://github.com/parse-community/parse-server/issues/9624)) ([bf9db75](https://github.com/parse-community/parse-server/commit/bf9db75e8685def1407034944725e758bc926c26))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* This upgrades the internally used Express framework from version 4 to 5, which may be a breaking change. If Parse Server is set up to be mounted on an Express application, we recommend to also use version 5 of the Express framework to avoid any compatibility issues. Note that even if there are no issues after upgrading, future releases of Parse Server may introduce issues if Parse Server internally relies on Express 5-specific features which are unsupported by the Express version on which it is mounted. See the Express [migration guide](https://expressjs.com/en/guide/migrating-5.html) and [release announcement](https://expressjs.com/2024/10/15/v5-release.html#breaking-changes) for more info. ([e0480df](e0480df))
|
||||
* This upgrades to the Parse JS SDK 6.0.0. See the [change log](https://github.com/parse-community/Parse-SDK-JS/releases/tag/6.0.0) of the Parse JS SDK for breaking changes and more details. ([bf9db75](bf9db75))
|
||||
* This removes the username from the email verification and password reset process to prevent storing personally identifiable information (PII) in server and infrastructure logs. Customized HTML pages or emails related to email verification and password reset may need to be adapted accordingly. See the new templates that come bundled with Parse Server and the [migration guide](https://github.com/parse-community/parse-server/blob/alpha/8.0.0.md) for more details. ([d21dd97](d21dd97))
|
||||
* This releases increases the required minimum versions to Postgres `15`, PostGIS `3.3` and removes support for Postgres `13`, `14`, PostGIS `3.1`, `3.2`. ([89c9b54](89c9b54))
|
||||
* The default value of Parse Server option `encodeParseObjectInCloudFunction` changes to `true`; the option has been deprecated and will be removed in a future version. ([5c5ad69](5c5ad69))
|
||||
* This releases increases the required minimum MongoDB versions to `6.0.19`, `7.0.16`, `8.0.4` and removes support for MongoDB `4`, `5`. ([871e508](871e508))
|
||||
* This releases increases the required minimum Node versions to 18.20.4, 20.18.0, 22.12.0 and removes unofficial support for Node 19. ([4e151cd](4e151cd))
|
||||
|
||||
# [7.4.0](https://github.com/parse-community/parse-server/compare/7.3.0...7.4.0) (2024-12-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `Parse.Query.distinct` fails due to invalid aggregate stage 'hint' ([#9295](https://github.com/parse-community/parse-server/issues/9295)) ([5f66c6a](https://github.com/parse-community/parse-server/commit/5f66c6a075cbe1cdaf9d1b108ee65af8ae596b89))
|
||||
* Security upgrade cross-spawn from 7.0.3 to 7.0.6 ([#9444](https://github.com/parse-community/parse-server/issues/9444)) ([3d034e0](https://github.com/parse-community/parse-server/commit/3d034e0a993e3e5bd9bb96a7e382bb3464f1eb68))
|
||||
* Security upgrade fast-xml-parser from 4.4.0 to 4.4.1 ([#9262](https://github.com/parse-community/parse-server/issues/9262)) ([992d39d](https://github.com/parse-community/parse-server/commit/992d39d508f230c774dcb764d1d907ec8887e6c5))
|
||||
* Security upgrade node from 20.14.0-alpine3.20 to 20.17.0-alpine3.20 ([#9300](https://github.com/parse-community/parse-server/issues/9300)) ([15bb17d](https://github.com/parse-community/parse-server/commit/15bb17d87153bf0d38f08fe4c720da29a204b36b))
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for MongoDB 8 ([#9269](https://github.com/parse-community/parse-server/issues/9269)) ([4756c66](https://github.com/parse-community/parse-server/commit/4756c66cd9f55afa1621d1a3f6fa850ed605cb53))
|
||||
* Add support for PostGIS 3.5 ([#9354](https://github.com/parse-community/parse-server/issues/9354)) ([8ea3538](https://github.com/parse-community/parse-server/commit/8ea35382db3436d54ab59bd30706705564b0985c))
|
||||
* Add support for Postgres 17 ([#9324](https://github.com/parse-community/parse-server/issues/9324)) ([fa2ee31](https://github.com/parse-community/parse-server/commit/fa2ee3196e4319a142b3838bb947c98dcba5d5cb))
|
||||
* Upgrade @parse/push-adapter from 6.7.1 to 6.8.0 ([#9489](https://github.com/parse-community/parse-server/issues/9489)) ([286aa66](https://github.com/parse-community/parse-server/commit/286aa664ac8830d36c3e70d2316917d15f0b6df5))
|
||||
|
||||
# [7.3.0](https://github.com/parse-community/parse-server/compare/7.2.0...7.3.0) (2024-10-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Custom object ID allows to acquire role privileges ([GHSA-8xq9-g7ch-35hg](https://github.com/parse-community/parse-server/security/advisories/GHSA-8xq9-g7ch-35hg)) ([#9317](https://github.com/parse-community/parse-server/issues/9317)) ([13ee52f](https://github.com/parse-community/parse-server/commit/13ee52f0d19ef3a3524b3d79aea100e587eb3cfc))
|
||||
* Parse Server `databaseOptions` nested keys incorrectly identified as invalid ([#9213](https://github.com/parse-community/parse-server/issues/9213)) ([77206d8](https://github.com/parse-community/parse-server/commit/77206d804443cfc1618c24f8961bd677de9920c0))
|
||||
* Parse Server installation fails due to post install script incorrectly parsing required min. Node version ([#9216](https://github.com/parse-community/parse-server/issues/9216)) ([0fa82a5](https://github.com/parse-community/parse-server/commit/0fa82a54fe38ec14e8054339285d3db71a8624c8))
|
||||
* Parse Server option `maxLogFiles` doesn't recognize day duration literals such as `1d` to mean 1 day ([#9215](https://github.com/parse-community/parse-server/issues/9215)) ([0319cee](https://github.com/parse-community/parse-server/commit/0319cee2dbf65e90bad377af1ed14ea25c595bf5))
|
||||
* Security upgrade path-to-regexp from 6.2.1 to 6.3.0 ([#9314](https://github.com/parse-community/parse-server/issues/9314)) ([8b7fe69](https://github.com/parse-community/parse-server/commit/8b7fe699c1c376ecd8cc1c97cce8e704ee41f28a))
|
||||
|
||||
### Features
|
||||
|
||||
* Add atomic operations for Cloud Config parameters ([#9219](https://github.com/parse-community/parse-server/issues/9219)) ([35cadf9](https://github.com/parse-community/parse-server/commit/35cadf9b8324879fb7309ba5d7ea46f2c722d614))
|
||||
* Add Cloud Code triggers `Parse.Cloud.beforeSave` and `Parse.Cloud.afterSave` for Parse Config ([#9232](https://github.com/parse-community/parse-server/issues/9232)) ([90a1e4a](https://github.com/parse-community/parse-server/commit/90a1e4a200423d644efb3f0ba2fba4b99f5cf954))
|
||||
* Add Node 22 support ([#9187](https://github.com/parse-community/parse-server/issues/9187)) ([7778471](https://github.com/parse-community/parse-server/commit/7778471999c7e42236ce404229660d80ecc2acd6))
|
||||
* Add support for asynchronous invocation of `FilesAdapter.getFileLocation` ([#9271](https://github.com/parse-community/parse-server/issues/9271)) ([1a2da40](https://github.com/parse-community/parse-server/commit/1a2da4055abe831b3017172fb75e16d7a8093873))
|
||||
|
||||
# [7.2.0](https://github.com/parse-community/parse-server/compare/7.1.0...7.2.0) (2024-07-09)
|
||||
|
||||
|
||||
|
||||
@@ -220,6 +220,7 @@ class CiVersionCheck {
|
||||
* Runs the check.
|
||||
*/
|
||||
async check() {
|
||||
/* eslint-disable no-console */
|
||||
try {
|
||||
console.log(`\nChecking ${this.packageName} versions in CI environments...`);
|
||||
|
||||
@@ -284,6 +285,7 @@ class CiVersionCheck {
|
||||
const msg = `Failed to check ${this.packageName} versions with error: ${e}`;
|
||||
core.setFailed(msg);
|
||||
}
|
||||
/* eslint-enable no-console */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ const util = require('util');
|
||||
fs.readFile('./src/Options/docs.js', 'utf8'),
|
||||
]);
|
||||
if (currentDefinitions !== newDefinitions || currentDocs !== newDocs) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
'\x1b[31m%s\x1b[0m',
|
||||
'Definitions files cannot be updated manually. Please update src/Options/index.js then run `npm run definitions` to generate definitions.'
|
||||
|
||||
@@ -87,6 +87,7 @@ class NodeEngineCheck {
|
||||
nodeVersion: version
|
||||
});
|
||||
} catch(e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Ignoring file because it is not valid JSON: ${file}`);
|
||||
core.warning(`Ignoring file because it is not valid JSON: ${file}`);
|
||||
}
|
||||
@@ -171,6 +172,7 @@ async function check() {
|
||||
// Get highest version
|
||||
const highestVersion = higherVersions.map(v => v.nodeMinVersion).pop();
|
||||
|
||||
/* eslint-disable no-console */
|
||||
// If there are higher versions
|
||||
if (higherVersions.length > 0) {
|
||||
console.log(`\nThere are ${higherVersions.length} dependencies that require a higher node engine version than the parent package (${parentVersion.nodeVersion}):`);
|
||||
@@ -189,6 +191,7 @@ async function check() {
|
||||
}
|
||||
|
||||
console.log(`✅ All dependencies satisfy the node version requirement of the parent package (${parentVersion.nodeVersion}).`);
|
||||
/* eslint-enable no-console */
|
||||
}
|
||||
|
||||
check();
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
const js = require("@eslint/js");
|
||||
const babelParser = require("@babel/eslint-parser");
|
||||
const globals = require("globals");
|
||||
module.exports = [
|
||||
{
|
||||
ignores: ["**/lib/**", "**/coverage/**", "**/out/**", "**/types/**"],
|
||||
},
|
||||
js.configs.recommended,
|
||||
{
|
||||
languageOptions: {
|
||||
parser: babelParser,
|
||||
ecmaVersion: 6,
|
||||
sourceType: "module",
|
||||
globals: {
|
||||
Parse: "readonly",
|
||||
...globals.node,
|
||||
},
|
||||
parserOptions: {
|
||||
requireConfigFile: false,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
indent: ["error", 2, { SwitchCase: 1 }],
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"no-trailing-spaces": "error",
|
||||
"eol-last": "error",
|
||||
"space-in-parens": ["error", "never"],
|
||||
"no-multiple-empty-lines": "warn",
|
||||
"prefer-const": "error",
|
||||
"space-infix-ops": "error",
|
||||
"no-useless-escape": "off",
|
||||
"require-atomic-updates": "off",
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
curly: ["error", "all"],
|
||||
"block-spacing": ["error", "always"],
|
||||
"no-unused-vars": "off",
|
||||
"no-console": "warn"
|
||||
},
|
||||
},
|
||||
];
|
||||
Generated
+9372
-6831
File diff suppressed because it is too large
Load Diff
+77
-74
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "parse-server",
|
||||
"version": "7.3.0-alpha.4",
|
||||
"version": "8.2.5",
|
||||
"description": "An express module providing a Parse-compatible API server",
|
||||
"main": "lib/index.js",
|
||||
"repository": {
|
||||
@@ -15,98 +15,103 @@
|
||||
"LICENSE",
|
||||
"NOTICE",
|
||||
"postinstall.js",
|
||||
"README.md"
|
||||
"README.md",
|
||||
"types"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@apollo/server": "4.10.1",
|
||||
"@babel/eslint-parser": "7.24.7",
|
||||
"@graphql-tools/merge": "9.0.3",
|
||||
"@graphql-tools/schema": "10.0.3",
|
||||
"@graphql-tools/utils": "8.12.0",
|
||||
"@apollo/server": "4.12.1",
|
||||
"@babel/eslint-parser": "7.28.0",
|
||||
"@graphql-tools/merge": "9.0.24",
|
||||
"@graphql-tools/schema": "10.0.23",
|
||||
"@graphql-tools/utils": "10.8.6",
|
||||
"@parse/fs-files-adapter": "3.0.0",
|
||||
"@parse/push-adapter": "6.4.1",
|
||||
"bcryptjs": "2.4.3",
|
||||
"body-parser": "1.20.2",
|
||||
"commander": "12.0.0",
|
||||
"@parse/push-adapter": "6.11.0",
|
||||
"bcryptjs": "3.0.2",
|
||||
"commander": "13.1.0",
|
||||
"cors": "2.8.5",
|
||||
"deepcopy": "2.1.0",
|
||||
"express": "4.19.2",
|
||||
"express-rate-limit": "7.3.1",
|
||||
"follow-redirects": "1.15.6",
|
||||
"graphql": "16.8.1",
|
||||
"express": "5.1.0",
|
||||
"express-rate-limit": "7.5.1",
|
||||
"follow-redirects": "1.15.9",
|
||||
"graphql": "16.11.0",
|
||||
"graphql-list-fields": "2.0.4",
|
||||
"graphql-relay": "0.10.1",
|
||||
"graphql-relay": "0.10.2",
|
||||
"graphql-tag": "2.12.6",
|
||||
"graphql-upload": "15.0.2",
|
||||
"intersect": "1.0.1",
|
||||
"jsonwebtoken": "9.0.2",
|
||||
"jwks-rsa": "3.1.0",
|
||||
"jwks-rsa": "3.2.0",
|
||||
"ldapjs": "3.0.7",
|
||||
"lodash": "4.17.21",
|
||||
"lru-cache": "10.2.2",
|
||||
"mime": "3.0.0",
|
||||
"mongodb": "5.9.0",
|
||||
"lru-cache": "10.4.0",
|
||||
"mime": "4.0.7",
|
||||
"mongodb": "6.17.0",
|
||||
"mustache": "4.2.0",
|
||||
"otpauth": "9.2.2",
|
||||
"parse": "5.3.0",
|
||||
"path-to-regexp": "6.2.1",
|
||||
"pg-monitor": "2.1.0",
|
||||
"pg-promise": "11.9.1",
|
||||
"otpauth": "9.4.0",
|
||||
"parse": "6.1.1",
|
||||
"path-to-regexp": "6.3.0",
|
||||
"pg-monitor": "3.0.0",
|
||||
"pg-promise": "11.14.0",
|
||||
"pluralize": "8.0.0",
|
||||
"punycode": "2.3.1",
|
||||
"rate-limit-redis": "4.2.0",
|
||||
"redis": "4.6.13",
|
||||
"semver": "7.6.0",
|
||||
"redis": "4.7.0",
|
||||
"router": "2.2.0",
|
||||
"semver": "7.7.2",
|
||||
"subscriptions-transport-ws": "0.11.0",
|
||||
"tv4": "1.3.0",
|
||||
"uuid": "10.0.0",
|
||||
"winston": "3.13.1",
|
||||
"uuid": "11.1.0",
|
||||
"winston": "3.17.0",
|
||||
"winston-daily-rotate-file": "5.0.0",
|
||||
"ws": "8.18.0"
|
||||
"ws": "8.18.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/core": "1.10.1",
|
||||
"@apollo/client": "3.9.11",
|
||||
"@babel/cli": "7.24.8",
|
||||
"@babel/core": "7.24.7",
|
||||
"@actions/core": "1.11.1",
|
||||
"@apollo/client": "3.13.8",
|
||||
"@babel/cli": "7.27.0",
|
||||
"@babel/core": "7.27.4",
|
||||
"@babel/plugin-proposal-object-rest-spread": "7.20.7",
|
||||
"@babel/plugin-transform-flow-strip-types": "7.24.7",
|
||||
"@babel/preset-env": "7.24.0",
|
||||
"@babel/plugin-transform-flow-strip-types": "7.26.5",
|
||||
"@babel/preset-env": "7.27.2",
|
||||
"@babel/preset-typescript": "7.27.1",
|
||||
"@saithodev/semantic-release-backmerge": "4.0.1",
|
||||
"@semantic-release/changelog": "6.0.3",
|
||||
"@semantic-release/commit-analyzer": "13.0.0",
|
||||
"@semantic-release/commit-analyzer": "13.0.1",
|
||||
"@semantic-release/git": "10.0.1",
|
||||
"@semantic-release/github": "10.1.0",
|
||||
"@semantic-release/github": "11.0.2",
|
||||
"@semantic-release/npm": "12.0.1",
|
||||
"@semantic-release/release-notes-generator": "14.0.1",
|
||||
"all-node-versions": "12.1.0",
|
||||
"apollo-upload-client": "17.0.0",
|
||||
"@semantic-release/release-notes-generator": "14.0.3",
|
||||
"all-node-versions": "13.0.1",
|
||||
"apollo-upload-client": "18.0.1",
|
||||
"clean-jsdoc-theme": "4.3.0",
|
||||
"cross-env": "7.0.2",
|
||||
"cross-env": "7.0.3",
|
||||
"deep-diff": "1.0.2",
|
||||
"eslint": "8.26.0",
|
||||
"eslint-plugin-flowtype": "8.0.3",
|
||||
"flow-bin": "0.119.1",
|
||||
"form-data": "4.0.0",
|
||||
"eslint": "9.27.0",
|
||||
"eslint-plugin-expect-type": "0.6.2",
|
||||
"flow-bin": "0.271.0",
|
||||
"form-data": "4.0.4",
|
||||
"globals": "16.2.0",
|
||||
"graphql-tag": "2.12.6",
|
||||
"husky": "9.0.11",
|
||||
"jasmine": "3.5.0",
|
||||
"husky": "9.1.7",
|
||||
"jasmine": "5.7.1",
|
||||
"jasmine-spec-reporter": "7.0.0",
|
||||
"jsdoc": "4.0.3",
|
||||
"jsdoc": "4.0.4",
|
||||
"jsdoc-babel": "0.5.0",
|
||||
"lint-staged": "10.2.3",
|
||||
"m": "1.9.0",
|
||||
"madge": "7.0.0",
|
||||
"lint-staged": "16.1.0",
|
||||
"m": "1.9.1",
|
||||
"madge": "8.0.0",
|
||||
"mock-files-adapter": "file:spec/dependencies/mock-files-adapter",
|
||||
"mock-mail-adapter": "file:spec/dependencies/mock-mail-adapter",
|
||||
"mongodb-runner": "5.5.4",
|
||||
"mongodb-runner": "5.9.3",
|
||||
"node-abort-controller": "3.1.1",
|
||||
"node-fetch": "3.2.10",
|
||||
"nyc": "15.1.0",
|
||||
"nyc": "17.1.0",
|
||||
"prettier": "2.0.5",
|
||||
"semantic-release": "24.0.0",
|
||||
"yaml": "1.10.0"
|
||||
"semantic-release": "24.2.5",
|
||||
"typescript": "5.8.3",
|
||||
"typescript-eslint": "8.33.1",
|
||||
"yaml": "2.8.0"
|
||||
},
|
||||
"scripts": {
|
||||
"ci:check": "node ./ci/ciCheck.js",
|
||||
@@ -114,38 +119,36 @@
|
||||
"ci:definitionsCheck": "node ./ci/definitionsCheck.js",
|
||||
"definitions": "node ./resources/buildConfigDefinitions.js && prettier --write 'src/Options/*.js'",
|
||||
"docs": "jsdoc -c ./jsdoc-conf.json",
|
||||
"lint": "flow && eslint --cache ./",
|
||||
"lint-fix": "eslint --fix --cache ./",
|
||||
"build": "babel src/ -d lib/ --copy-files",
|
||||
"lint": "eslint --cache ./ --flag unstable_config_lookup_from_file",
|
||||
"lint-fix": "eslint --fix --cache ./ --flag unstable_config_lookup_from_file",
|
||||
"build": "babel src/ -d lib/ --copy-files --extensions '.ts,.js'",
|
||||
"build:types": "tsc",
|
||||
"watch": "babel --watch src/ -d lib/ --copy-files",
|
||||
"test:mongodb:runnerstart": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=$npm_config_dbversion} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner start -t ${MONGODB_TOPOLOGY} --version ${MONGODB_VERSION} -- --port 27017",
|
||||
"test:mongodb:testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=$npm_config_dbversion} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 jasmine",
|
||||
"test:mongodb": "npm run test:mongodb:runnerstart --dbversion=$npm_config_dbversion && npm run test:mongodb:testonly --dbversion=$npm_config_dbversion",
|
||||
"test:mongodb:4.2.19": "npm run test:mongodb --dbversion=4.2.19",
|
||||
"test:mongodb:4.4.13": "npm run test:mongodb --dbversion=4.4.13",
|
||||
"test:mongodb:5.3.2": "npm run test:mongodb --dbversion=5.3.2",
|
||||
"test:mongodb:6.0.2": "npm run test:mongodb --dbversion=6.0.2",
|
||||
"test:mongodb:7.0.1": "npm run test:mongodb --dbversion=7.0.1",
|
||||
"watch:ts": "tsc --watch",
|
||||
"test:mongodb:6.0.19": "MONGODB_VERSION=6.0.19 npm run test",
|
||||
"test:mongodb:7.0.16": "MONGODB_VERSION=7.0.16 npm run test",
|
||||
"test:mongodb:8.0.4": "MONGODB_VERSION=8.0.4 npm run test",
|
||||
"test:postgres:testonly": "cross-env PARSE_SERVER_TEST_DB=postgres PARSE_SERVER_TEST_DATABASE_URI=postgres://postgres:password@localhost:5432/parse_server_postgres_adapter_test_database npm run testonly",
|
||||
"pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner start -t ${MONGODB_TOPOLOGY} --version ${MONGODB_VERSION} -- --port 27017",
|
||||
"testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 jasmine",
|
||||
"test": "npm run testonly",
|
||||
"posttest": "cross-env mongodb-runner stop --all",
|
||||
"coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 nyc jasmine",
|
||||
"testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=8.0.4} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 jasmine",
|
||||
"test": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=8.0.4} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner exec -t ${MONGODB_TOPOLOGY} --version ${MONGODB_VERSION} -- --port 27017 -- npm run testonly",
|
||||
"test:types": "eslint types/tests.ts -c ./types/eslint.config.mjs",
|
||||
"coverage:mongodb": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=8.0.4} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner exec -t ${MONGODB_TOPOLOGY} --version ${MONGODB_VERSION} -- --port 27017 -- npm run coverage",
|
||||
"coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=8.0.4} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 nyc jasmine",
|
||||
"start": "node ./bin/parse-server",
|
||||
"prettier": "prettier --write {src,spec}/{**/*,*}.js",
|
||||
"prepare": "npm run build",
|
||||
"postinstall": "node -p 'require(\"./postinstall.js\")()'",
|
||||
"madge:circular": "node_modules/.bin/madge ./src --circular"
|
||||
},
|
||||
"types": "types/index.d.ts",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0 || >=19.0.0 <20.0.0 || >=20.0.0 <21.0.0 || >=22.0.0 <23.0.0"
|
||||
"node": ">=18.20.4 <19.0.0 || >=20.18.0 <21.0.0 || >=22.12.0 <23.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"parse-server": "bin/parse-server"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@node-rs/bcrypt": "1.10.4"
|
||||
"@node-rs/bcrypt": "1.10.7"
|
||||
},
|
||||
"collective": {
|
||||
"type": "opencollective",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<h1>{{appName}}</h1>
|
||||
<h1>Expired verification link!</h1>
|
||||
<form method="POST" action="{{{publicServerUrl}}}/apps/{{{appId}}}/resend_verification_email">
|
||||
<input name="username" type="hidden" value="{{{username}}}">
|
||||
<input name="token" type="hidden" value="{{{token}}}">
|
||||
<input name="locale" type="hidden" value="{{{locale}}}">
|
||||
<button type="submit">Resend Link</button>
|
||||
</form>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<h1>{{appName}}</h1>
|
||||
<h1>Expired verification link!</h1>
|
||||
<form method="POST" action="{{{publicServerUrl}}}/apps/{{{appId}}}/resend_verification_email">
|
||||
<input name="username" type="hidden" value="{{{username}}}">
|
||||
<input name="token" type="hidden" value="{{{token}}}">
|
||||
<input name="locale" type="hidden" value="{{{locale}}}">
|
||||
<button type="submit">Resend Link</button>
|
||||
</form>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<h1>{{appName}}</h1>
|
||||
<h1>Expired verification link!</h1>
|
||||
<form method="POST" action="{{{publicServerUrl}}}/apps/{{{appId}}}/resend_verification_email">
|
||||
<input name="username" type="hidden" value="{{{username}}}">
|
||||
<input name="token" type="hidden" value="{{{token}}}">
|
||||
<input name="locale" type="hidden" value="{{{locale}}}">
|
||||
<button type="submit">Resend Link</button>
|
||||
</form>
|
||||
|
||||
@@ -47,8 +47,8 @@
|
||||
window.onload = addDataToForm;
|
||||
|
||||
function addDataToForm() {
|
||||
var username = getUrlParameter("username");
|
||||
document.getElementById("usernameField").value = username;
|
||||
const token = getUrlParameter("token");
|
||||
document.getElementById("token").value = token;
|
||||
|
||||
var appId = getUrlParameter("appId");
|
||||
document.getElementById("resendForm").action = '/apps/' + appId + '/resend_verification_email'
|
||||
@@ -60,7 +60,7 @@
|
||||
<div class="container">
|
||||
<h1>Invalid Verification Link</h1>
|
||||
<form id="resendForm" method="POST" action="/resend_verification_email">
|
||||
<input id="usernameField" class="form-control" name="username" type="hidden" value="">
|
||||
<input id="token" class="form-control" name="token" type="hidden" value="">
|
||||
<button type="submit" class="btn btn-default">Resend Link</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -161,6 +161,9 @@ function mapperFor(elt, t) {
|
||||
if (type == 'NumberOrBoolean') {
|
||||
return wrap(t.identifier('numberOrBooleanParser'));
|
||||
}
|
||||
if (type == 'NumberOrString') {
|
||||
return t.callExpression(wrap(t.identifier('numberOrStringParser')), [t.stringLiteral(elt.name)]);
|
||||
}
|
||||
if (type === 'StringOrStringArray') {
|
||||
return wrap(t.identifier('arrayParser'));
|
||||
}
|
||||
@@ -212,6 +215,9 @@ function parseDefaultValue(elt, value, t) {
|
||||
if (type == 'NumberOrBoolean') {
|
||||
literalValue = t.numericLiteral(parsers.numberOrBoolParser('')(value));
|
||||
}
|
||||
if (type == 'NumberOrString') {
|
||||
literalValue = t.numericLiteral(parsers.numberOrStringParser('')(value));
|
||||
}
|
||||
|
||||
if (nestedOptionTypes.includes(type)) {
|
||||
const object = parsers.objectParser(value);
|
||||
|
||||
+3
-3
@@ -3,12 +3,12 @@
|
||||
"@babel/plugin-proposal-object-rest-spread"
|
||||
],
|
||||
"presets": [
|
||||
"@babel/preset-typescript",
|
||||
["@babel/preset-env", {
|
||||
"targets": {
|
||||
"node": "14"
|
||||
"node": "18"
|
||||
}
|
||||
}]
|
||||
],
|
||||
"sourceMaps": "inline",
|
||||
"retainLines": true
|
||||
"sourceMaps": "inline"
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"jasmine": true
|
||||
},
|
||||
"globals": {
|
||||
"Parse": true,
|
||||
"reconfigureServer": true,
|
||||
"createTestUser": true,
|
||||
"jfail": true,
|
||||
"ok": true,
|
||||
"strictEqual": true,
|
||||
"TestObject": true,
|
||||
"Item": true,
|
||||
"Container": true,
|
||||
"equal": true,
|
||||
"expectAsync": true,
|
||||
"notEqual": true,
|
||||
"it_id": true,
|
||||
"it_only_db": true,
|
||||
"it_only_mongodb_version": true,
|
||||
"it_only_postgres_version": true,
|
||||
"it_only_node_version": true,
|
||||
"fit_only_mongodb_version": true,
|
||||
"fit_only_node_version": true,
|
||||
"it_exclude_mongodb_version": true,
|
||||
"it_exclude_postgres_version": true,
|
||||
"fit_exclude_mongodb_version": true,
|
||||
"fit_exclude_node_version": true,
|
||||
"it_exclude_dbs": true,
|
||||
"describe_only_db": true,
|
||||
"fdescribe_only_db": true,
|
||||
"describe_only": true,
|
||||
"on_db": true,
|
||||
"defaultConfiguration": true,
|
||||
"range": true,
|
||||
"jequal": true,
|
||||
"create": true,
|
||||
"arrayContains": true,
|
||||
"databaseAdapter": true,
|
||||
"databaseURI": true
|
||||
},
|
||||
"rules": {
|
||||
"no-console": [0],
|
||||
"no-var": "error"
|
||||
}
|
||||
}
|
||||
@@ -419,7 +419,7 @@ describe('lockout with password reset option', () => {
|
||||
await request({
|
||||
method: 'POST',
|
||||
url: `${config.publicServerURL}/apps/test/request_password_reset`,
|
||||
body: `new_password=${newPassword}&token=${token}&username=${username}`,
|
||||
body: `new_password=${newPassword}&token=${token}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
@@ -454,7 +454,7 @@ describe('lockout with password reset option', () => {
|
||||
await request({
|
||||
method: 'POST',
|
||||
url: `${config.publicServerURL}/apps/test/request_password_reset`,
|
||||
body: `new_password=${newPassword}&token=${token}&username=${username}`,
|
||||
body: `new_password=${newPassword}&token=${token}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
const BaseAuthCodeAdapter = require('../../../lib/Adapters/Auth/BaseCodeAuthAdapter').default;
|
||||
|
||||
describe('BaseAuthCodeAdapter', function () {
|
||||
let adapter;
|
||||
const adapterName = 'TestAdapter';
|
||||
const validOptions = {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
};
|
||||
|
||||
class TestAuthCodeAdapter extends BaseAuthCodeAdapter {
|
||||
async getUserFromAccessToken(accessToken) {
|
||||
if (accessToken === 'validAccessToken') {
|
||||
return { id: 'validUserId' };
|
||||
}
|
||||
throw new Error('Invalid access token');
|
||||
}
|
||||
|
||||
async getAccessTokenFromCode(authData) {
|
||||
if (authData.code === 'validCode') {
|
||||
return 'validAccessToken';
|
||||
}
|
||||
throw new Error('Invalid code');
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new TestAuthCodeAdapter(adapterName);
|
||||
});
|
||||
|
||||
describe('validateOptions', function () {
|
||||
it('should throw error if options are missing', function () {
|
||||
expect(() => adapter.validateOptions(null)).toThrowError(`${adapterName} options are required.`);
|
||||
});
|
||||
|
||||
it('should throw error if clientId is missing in secure mode', function () {
|
||||
expect(() =>
|
||||
adapter.validateOptions({ clientSecret: 'validClientSecret' })
|
||||
).toThrowError(`${adapterName} clientId is required.`);
|
||||
});
|
||||
|
||||
it('should throw error if clientSecret is missing in secure mode', function () {
|
||||
expect(() =>
|
||||
adapter.validateOptions({ clientId: 'validClientId' })
|
||||
).toThrowError(`${adapterName} clientSecret is required.`);
|
||||
});
|
||||
|
||||
it('should not throw error for valid options', function () {
|
||||
expect(() => adapter.validateOptions(validOptions)).not.toThrow();
|
||||
expect(adapter.clientId).toBe('validClientId');
|
||||
expect(adapter.clientSecret).toBe('validClientSecret');
|
||||
expect(adapter.enableInsecureAuth).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should allow insecure mode without clientId or clientSecret', function () {
|
||||
const options = { enableInsecureAuth: true };
|
||||
expect(() => adapter.validateOptions(options)).not.toThrow();
|
||||
expect(adapter.enableInsecureAuth).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('beforeFind', function () {
|
||||
it('should throw error if code is missing in secure mode', async function () {
|
||||
adapter.validateOptions(validOptions);
|
||||
const authData = { access_token: 'validAccessToken' };
|
||||
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeRejectedWithError(
|
||||
`${adapterName} code is required.`
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error if access token is missing in insecure mode', async function () {
|
||||
adapter.validateOptions({ enableInsecureAuth: true });
|
||||
const authData = {};
|
||||
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeRejectedWithError(
|
||||
`${adapterName} auth is invalid for this user.`
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error if user ID does not match in insecure mode', async function () {
|
||||
adapter.validateOptions({ enableInsecureAuth: true });
|
||||
const authData = { id: 'invalidUserId', access_token: 'validAccessToken' };
|
||||
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeRejectedWithError(
|
||||
`${adapterName} auth is invalid for this user.`
|
||||
);
|
||||
});
|
||||
|
||||
it('should process valid secure payload and update authData', async function () {
|
||||
adapter.validateOptions(validOptions);
|
||||
const authData = { code: 'validCode' };
|
||||
|
||||
await adapter.beforeFind(authData);
|
||||
|
||||
expect(authData.access_token).toBe('validAccessToken');
|
||||
expect(authData.id).toBe('validUserId');
|
||||
expect(authData.code).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should process valid insecure payload', async function () {
|
||||
adapter.validateOptions({ enableInsecureAuth: true });
|
||||
const authData = { id: 'validUserId', access_token: 'validAccessToken' };
|
||||
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeResolved();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserFromAccessToken', function () {
|
||||
it('should throw error if not implemented in base class', async function () {
|
||||
const baseAdapter = new BaseAuthCodeAdapter(adapterName);
|
||||
|
||||
await expectAsync(baseAdapter.getUserFromAccessToken('test')).toBeRejectedWithError(
|
||||
'getUserFromAccessToken is not implemented'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return valid user for valid access token', async function () {
|
||||
const user = await adapter.getUserFromAccessToken('validAccessToken', {});
|
||||
expect(user).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
|
||||
it('should throw error for invalid access token', async function () {
|
||||
await expectAsync(adapter.getUserFromAccessToken('invalidAccessToken', {})).toBeRejectedWithError(
|
||||
'Invalid access token'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAccessTokenFromCode', function () {
|
||||
it('should throw error if not implemented in base class', async function () {
|
||||
const baseAdapter = new BaseAuthCodeAdapter(adapterName);
|
||||
|
||||
await expectAsync(baseAdapter.getAccessTokenFromCode({ code: 'test' })).toBeRejectedWithError(
|
||||
'getAccessTokenFromCode is not implemented'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return valid access token for valid code', async function () {
|
||||
const accessToken = await adapter.getAccessTokenFromCode({ code: 'validCode' });
|
||||
expect(accessToken).toBe('validAccessToken');
|
||||
});
|
||||
|
||||
it('should throw error for invalid code', async function () {
|
||||
await expectAsync(adapter.getAccessTokenFromCode({ code: 'invalidCode' })).toBeRejectedWithError(
|
||||
'Invalid code'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateLogin', function () {
|
||||
it('should return user id from authData', function () {
|
||||
const authData = { id: 'validUserId' };
|
||||
const result = adapter.validateLogin(authData);
|
||||
expect(result).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateSetUp', function () {
|
||||
it('should return user id from authData', function () {
|
||||
const authData = { id: 'validUserId' };
|
||||
const result = adapter.validateSetUp(authData);
|
||||
expect(result).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('afterFind', function () {
|
||||
it('should return user id from authData', function () {
|
||||
const authData = { id: 'validUserId' };
|
||||
const result = adapter.afterFind(authData);
|
||||
expect(result).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateUpdate', function () {
|
||||
it('should return user id from authData', function () {
|
||||
const authData = { id: 'validUserId' };
|
||||
const result = adapter.validateUpdate(authData);
|
||||
expect(result).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,220 @@
|
||||
const GameCenterAuth = require('../../../lib/Adapters/Auth/gcenter').default;
|
||||
const { pki } = require('node-forge');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
describe('GameCenterAuth Adapter', function () {
|
||||
let adapter;
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new GameCenterAuth.constructor();
|
||||
|
||||
const gcProd4 = fs.readFileSync(path.resolve(__dirname, '../../support/cert/gc-prod-4.cer'));
|
||||
const digicertPem = fs.readFileSync(path.resolve(__dirname, '../../support/cert/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem')).toString();
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
headers: new Map(),
|
||||
arrayBuffer: () => Promise.resolve(
|
||||
gcProd4.buffer.slice(gcProd4.byteOffset, gcProd4.byteOffset + gcProd4.length)
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
headers: new Map([['content-type', 'application/x-pem-file'], ['content-length', digicertPem.length.toString()]]),
|
||||
text: () => Promise.resolve(digicertPem),
|
||||
},
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
describe('Test config failing due to missing params or wrong types', function () {
|
||||
it('should throw error for invalid options', async function () {
|
||||
const invalidOptions = [
|
||||
null,
|
||||
undefined,
|
||||
{},
|
||||
{ bundleId: '' },
|
||||
{ enableInsecureAuth: false }, // Missing bundleId in secure mode
|
||||
];
|
||||
|
||||
for (const options of invalidOptions) {
|
||||
expect(() => adapter.validateOptions(options)).withContext(JSON.stringify(options)).toThrow()
|
||||
}
|
||||
});
|
||||
|
||||
it('should validate options successfully with valid parameters', function () {
|
||||
const validOptions = { bundleId: 'com.valid.app', enableInsecureAuth: false };
|
||||
expect(() => adapter.validateOptions(validOptions)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test payload failing due to missing params or wrong types', function () {
|
||||
it('should throw error for missing authData fields', async function () {
|
||||
await expectAsync(adapter.validateAuthData({})).toBeRejectedWithError(
|
||||
'AuthData id is missing.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test payload fails due to incorrect appId / certificate', function () {
|
||||
it('should throw error for invalid publicKeyUrl', async function () {
|
||||
const invalidPublicKeyUrl = 'https://malicious.url.com/key.cer';
|
||||
|
||||
spyOn(adapter, 'fetchCertificate').and.throwError(
|
||||
new Error('Invalid publicKeyUrl')
|
||||
);
|
||||
|
||||
await expectAsync(
|
||||
adapter.getAppleCertificate(invalidPublicKeyUrl)
|
||||
).toBeRejectedWithError('Invalid publicKeyUrl: https://malicious.url.com/key.cer');
|
||||
});
|
||||
|
||||
it('should throw error for invalid signature verification', async function () {
|
||||
const fakePublicKey = 'invalid-key';
|
||||
const fakeAuthData = {
|
||||
id: '1234567',
|
||||
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||
timestamp: 1460981421303,
|
||||
salt: 'saltST==',
|
||||
signature: 'invalidSignature',
|
||||
};
|
||||
|
||||
spyOn(adapter, 'getAppleCertificate').and.returnValue(Promise.resolve(fakePublicKey));
|
||||
spyOn(adapter, 'verifySignature').and.throwError('Invalid signature.');
|
||||
|
||||
await expectAsync(adapter.validateAuthData(fakeAuthData)).toBeRejectedWithError(
|
||||
'Invalid signature.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test payload passing', function () {
|
||||
it('should successfully process valid payload and save auth data', async function () {
|
||||
const validAuthData = {
|
||||
id: '1234567',
|
||||
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||
timestamp: 1460981421303,
|
||||
salt: 'saltST==',
|
||||
signature: 'validSignature',
|
||||
bundleId: 'com.valid.app',
|
||||
};
|
||||
|
||||
spyOn(adapter, 'getAppleCertificate').and.returnValue(Promise.resolve('validKey'));
|
||||
spyOn(adapter, 'verifySignature').and.returnValue(true);
|
||||
|
||||
await expectAsync(adapter.validateAuthData(validAuthData)).toBeResolved();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Certificate and Signature Validation', function () {
|
||||
it('should fetch and validate Apple certificate', async function () {
|
||||
const certUrl = 'https://static.gc.apple.com/public-key/gc-prod-4.cer';
|
||||
const mockCertificate = 'mockCertificate';
|
||||
|
||||
spyOn(adapter, 'fetchCertificate').and.returnValue(
|
||||
Promise.resolve({ certificate: mockCertificate, headers: new Map() })
|
||||
);
|
||||
spyOn(pki, 'certificateFromPem').and.returnValue({});
|
||||
|
||||
adapter.cache[certUrl] = mockCertificate;
|
||||
|
||||
const cert = await adapter.getAppleCertificate(certUrl);
|
||||
expect(cert).toBe(mockCertificate);
|
||||
});
|
||||
|
||||
it('should verify signature successfully', async function () {
|
||||
const authData = {
|
||||
id: 'G:1965586982',
|
||||
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||
timestamp: 1565257031287,
|
||||
signature:
|
||||
'uqLBTr9Uex8zCpc1UQ1MIDMitb+HUat2Mah4Kw6AVLSGe0gGNJXlih2i5X+0ZwVY0S9zY2NHWi2gFjmhjt/4kxWGMkupqXX5H/qhE2m7hzox6lZJpH98ZEUbouWRfZX2ZhUlCkAX09oRNi7fI7mWL1/o88MaI/y6k6tLr14JTzmlxgdyhw+QRLxRPA6NuvUlRSJpyJ4aGtNH5/wHdKQWL8nUnFYiYmaY8R7IjzNxPfy8UJTUWmeZvMSgND4u8EjADPsz7ZtZyWAPi8kYcAb6M8k0jwLD3vrYCB8XXyO2RQb/FY2TM4zJuI7PzLlvvgOJXbbfVtHx7Evnm5NYoyzgzw==',
|
||||
salt: 'DzqqrQ==',
|
||||
};
|
||||
|
||||
adapter.bundleId = 'cloud.xtralife.gamecenterauth';
|
||||
adapter.enableInsecureAuth = false;
|
||||
|
||||
spyOn(adapter, 'verifyPublicKeyIssuer').and.returnValue();
|
||||
|
||||
const publicKey = await adapter.getAppleCertificate(authData.publicKeyUrl);
|
||||
|
||||
expect(() => adapter.verifySignature(publicKey, authData)).not.toThrow();
|
||||
|
||||
});
|
||||
|
||||
it('should not use bundle id from authData payload in secure mode', async function () {
|
||||
const authData = {
|
||||
id: 'G:1965586982',
|
||||
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||
timestamp: 1565257031287,
|
||||
signature:
|
||||
'uqLBTr9Uex8zCpc1UQ1MIDMitb+HUat2Mah4Kw6AVLSGe0gGNJXlih2i5X+0ZwVY0S9zY2NHWi2gFjmhjt/4kxWGMkupqXX5H/qhE2m7hzox6lZJpH98ZEUbouWRfZX2ZhUlCkAX09oRNi7fI7mWL1/o88MaI/y6k6tLr14JTzmlxgdyhw+QRLxRPA6NuvUlRSJpyJ4aGtNH5/wHdKQWL8nUnFYiYmaY8R7IjzNxPfy8UJTUWmeZvMSgND4u8EjADPsz7ZtZyWAPi8kYcAb6M8k0jwLD3vrYCB8XXyO2RQb/FY2TM4zJuI7PzLlvvgOJXbbfVtHx7Evnm5NYoyzgzw==',
|
||||
salt: 'DzqqrQ==',
|
||||
bundleId: 'com.example.insecure.app',
|
||||
};
|
||||
|
||||
adapter.bundleId = 'cloud.xtralife.gamecenterauth';
|
||||
adapter.enableInsecureAuth = false;
|
||||
|
||||
spyOn(adapter, 'verifyPublicKeyIssuer').and.returnValue();
|
||||
|
||||
const publicKey = await adapter.getAppleCertificate(authData.publicKeyUrl);
|
||||
|
||||
expect(() => adapter.verifySignature(publicKey, authData)).not.toThrow();
|
||||
|
||||
});
|
||||
|
||||
it('should not use bundle id from authData payload in insecure mode', async function () {
|
||||
const authData = {
|
||||
id: 'G:1965586982',
|
||||
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||
timestamp: 1565257031287,
|
||||
signature:
|
||||
'uqLBTr9Uex8zCpc1UQ1MIDMitb+HUat2Mah4Kw6AVLSGe0gGNJXlih2i5X+0ZwVY0S9zY2NHWi2gFjmhjt/4kxWGMkupqXX5H/qhE2m7hzox6lZJpH98ZEUbouWRfZX2ZhUlCkAX09oRNi7fI7mWL1/o88MaI/y6k6tLr14JTzmlxgdyhw+QRLxRPA6NuvUlRSJpyJ4aGtNH5/wHdKQWL8nUnFYiYmaY8R7IjzNxPfy8UJTUWmeZvMSgND4u8EjADPsz7ZtZyWAPi8kYcAb6M8k0jwLD3vrYCB8XXyO2RQb/FY2TM4zJuI7PzLlvvgOJXbbfVtHx7Evnm5NYoyzgzw==',
|
||||
salt: 'DzqqrQ==',
|
||||
bundleId: 'com.example.insecure.app',
|
||||
};
|
||||
|
||||
adapter.bundleId = 'cloud.xtralife.gamecenterauth';
|
||||
adapter.enableInsecureAuth = true;
|
||||
|
||||
spyOn(adapter, 'verifyPublicKeyIssuer').and.returnValue();
|
||||
|
||||
const publicKey = await adapter.getAppleCertificate(authData.publicKeyUrl);
|
||||
|
||||
expect(() => adapter.verifySignature(publicKey, authData)).not.toThrow();
|
||||
|
||||
});
|
||||
|
||||
it('can use bundle id from authData payload in insecure mode', async function () {
|
||||
const authData = {
|
||||
id: 'G:1965586982',
|
||||
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||
timestamp: 1565257031287,
|
||||
signature:
|
||||
'uqLBTr9Uex8zCpc1UQ1MIDMitb+HUat2Mah4Kw6AVLSGe0gGNJXlih2i5X+0ZwVY0S9zY2NHWi2gFjmhjt/4kxWGMkupqXX5H/qhE2m7hzox6lZJpH98ZEUbouWRfZX2ZhUlCkAX09oRNi7fI7mWL1/o88MaI/y6k6tLr14JTzmlxgdyhw+QRLxRPA6NuvUlRSJpyJ4aGtNH5/wHdKQWL8nUnFYiYmaY8R7IjzNxPfy8UJTUWmeZvMSgND4u8EjADPsz7ZtZyWAPi8kYcAb6M8k0jwLD3vrYCB8XXyO2RQb/FY2TM4zJuI7PzLlvvgOJXbbfVtHx7Evnm5NYoyzgzw==',
|
||||
salt: 'DzqqrQ==',
|
||||
bundleId: 'cloud.xtralife.gamecenterauth',
|
||||
};
|
||||
|
||||
adapter.enableInsecureAuth = true;
|
||||
|
||||
spyOn(adapter, 'verifyPublicKeyIssuer').and.returnValue();
|
||||
|
||||
const publicKey = await adapter.getAppleCertificate(authData.publicKeyUrl);
|
||||
|
||||
expect(() => adapter.verifySignature(publicKey, authData)).not.toThrow();
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,285 @@
|
||||
const GitHubAdapter = require('../../../lib/Adapters/Auth/github').default;
|
||||
|
||||
describe('GitHubAdapter', function () {
|
||||
let adapter;
|
||||
const validOptions = {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new GitHubAdapter.constructor();
|
||||
adapter.validateOptions(validOptions);
|
||||
});
|
||||
|
||||
describe('getAccessTokenFromCode', function () {
|
||||
it('should fetch an access token successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://github.com/login/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const code = 'validCode';
|
||||
const token = await adapter.getAccessTokenFromCode(code);
|
||||
|
||||
expect(token).toBe('mockAccessToken');
|
||||
});
|
||||
|
||||
it('should throw an error if the response is not ok', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://github.com/login/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Bad Request',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const code = 'invalidCode';
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(code)).toBeRejectedWithError(
|
||||
'Failed to exchange code for token: Bad Request'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if the response contains an error', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://github.com/login/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
error: 'invalid_grant',
|
||||
error_description: 'Code is invalid',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const code = 'invalidCode';
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(code)).toBeRejectedWithError('Code is invalid');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserFromAccessToken', function () {
|
||||
it('should fetch user data successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.github.com/user',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'mockUserId',
|
||||
login: 'mockUserLogin',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'validAccessToken';
|
||||
const user = await adapter.getUserFromAccessToken(accessToken);
|
||||
|
||||
expect(user).toEqual({ id: 'mockUserId', login: 'mockUserLogin' });
|
||||
});
|
||||
|
||||
it('should throw an error if the response is not ok', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.github.com/user',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'invalidAccessToken';
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken(accessToken)).toBeRejectedWithError(
|
||||
'Failed to fetch GitHub user: Unauthorized'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if user data is invalid', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.github.com/user',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'validAccessToken';
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken(accessToken)).toBeRejectedWithError(
|
||||
'Invalid GitHub user data received.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GitHubAdapter E2E Test', function () {
|
||||
beforeEach(async function () {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
github: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should log in user using GitHub adapter successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://github.com/login/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.github.com/user',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'mockUserId',
|
||||
login: 'mockUserLogin',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode' };
|
||||
const user = await Parse.User.logInWith('github', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle error when GitHub returns invalid code', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://github.com/login/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Invalid code',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'invalidCode' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('github', { authData })).toBeRejectedWithError(
|
||||
'Failed to exchange code for token: Invalid code'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when GitHub returns invalid user data', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://github.com/login/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.github.com/user',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('github', { authData })).toBeRejectedWithError(
|
||||
'Failed to fetch GitHub user: Unauthorized'
|
||||
);
|
||||
});
|
||||
|
||||
it('e2e secure does not support insecure payload', async function () {
|
||||
mockFetch();
|
||||
const authData = { id: 'mockUserId', access_token: 'mockAccessToken123' };
|
||||
await expectAsync(Parse.User.logInWith('github', { authData })).toBeRejectedWithError(
|
||||
'GitHub code is required.'
|
||||
);
|
||||
});
|
||||
|
||||
it('e2e insecure does support secure payload', async function () {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
github: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
enableInsecureAuth: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://github.com/login/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.github.com/user',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'mockUserId',
|
||||
login: 'mockUserLogin',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode' };
|
||||
const user = await Parse.User.logInWith('github', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,356 @@
|
||||
const GooglePlayGamesServicesAdapter = require('../../../lib/Adapters/Auth/gpgames').default;
|
||||
|
||||
describe('GooglePlayGamesServicesAdapter', function () {
|
||||
let adapter;
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new GooglePlayGamesServicesAdapter.constructor();
|
||||
adapter.clientId = 'validClientId';
|
||||
adapter.clientSecret = 'validClientSecret';
|
||||
});
|
||||
|
||||
describe('getAccessTokenFromCode', function () {
|
||||
it('should fetch an access token successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://oauth2.googleapis.com/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const code = 'validCode';
|
||||
const authData = { redirectUri: 'http://example.com' };
|
||||
const token = await adapter.getAccessTokenFromCode(code, authData);
|
||||
|
||||
expect(token).toBe('mockAccessToken');
|
||||
});
|
||||
|
||||
it('should throw an error if the response is not ok', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://oauth2.googleapis.com/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Bad Request',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const code = 'invalidCode';
|
||||
const authData = { redirectUri: 'http://example.com' };
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(code, authData)).toBeRejectedWithError(
|
||||
'Failed to exchange code for token: Bad Request'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if the response contains an error', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://oauth2.googleapis.com/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
error: 'invalid_grant',
|
||||
error_description: 'Code is invalid',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const code = 'invalidCode';
|
||||
const authData = { redirectUri: 'http://example.com' };
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(code, authData)).toBeRejectedWithError(
|
||||
'Code is invalid'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserFromAccessToken', function () {
|
||||
it('should fetch user data successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
playerId: 'mockUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'validAccessToken';
|
||||
const authData = { id: 'mockUserId' };
|
||||
const user = await adapter.getUserFromAccessToken(accessToken, authData);
|
||||
|
||||
expect(user).toEqual({ id: 'mockUserId' });
|
||||
});
|
||||
|
||||
it('should throw an error if the response is not ok', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'invalidAccessToken';
|
||||
const authData = { id: 'mockUserId' };
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken(accessToken, authData)).toBeRejectedWithError(
|
||||
'Failed to fetch Google Play Games Services user: Unauthorized'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if user data is invalid', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'validAccessToken';
|
||||
const authData = { id: 'mockUserId' };
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken(accessToken, authData)).toBeRejectedWithError(
|
||||
'Invalid Google Play Games Services user data received.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if playerId does not match the provided user ID', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
playerId: 'anotherUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'validAccessToken';
|
||||
const authData = { id: 'mockUserId' };
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken(accessToken, authData)).toBeRejectedWithError(
|
||||
'Invalid Google Play Games Services user data received.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GooglePlayGamesServicesAdapter E2E Test', function () {
|
||||
beforeEach(async function () {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
gpgames: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should log in user successfully with valid code', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://oauth2.googleapis.com/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
playerId: 'mockUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
id: 'mockUserId',
|
||||
redirectUri: 'http://example.com',
|
||||
};
|
||||
|
||||
const user = await Parse.User.logInWith('gpgames', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://oauth2.googleapis.com/token',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when the token exchange fails', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://oauth2.googleapis.com/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Invalid code',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'invalidCode',
|
||||
redirectUri: 'http://example.com',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('gpgames', { authData })).toBeRejectedWithError(
|
||||
'Failed to exchange code for token: Invalid code'
|
||||
);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://oauth2.googleapis.com/token',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when user data fetch fails', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://oauth2.googleapis.com/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
id: 'mockUserId',
|
||||
redirectUri: 'http://example.com',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('gpgames', { authData })).toBeRejectedWithError(
|
||||
'Failed to fetch Google Play Games Services user: Unauthorized'
|
||||
);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://oauth2.googleapis.com/token',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when user data is invalid', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://oauth2.googleapis.com/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
playerId: 'anotherUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
id: 'mockUserId',
|
||||
redirectUri: 'http://example.com',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('gpgames', { authData })).toBeRejectedWithError(
|
||||
'Invalid Google Play Games Services user data received.'
|
||||
);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://oauth2.googleapis.com/token',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when no code or access token is provided', async function () {
|
||||
mockFetch();
|
||||
|
||||
const authData = {
|
||||
id: 'mockUserId',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('gpgames', { authData })).toBeRejectedWithError(
|
||||
'gpgames code is required.'
|
||||
);
|
||||
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
const InstagramAdapter = require('../../../lib/Adapters/Auth/instagram').default;
|
||||
|
||||
describe('InstagramAdapter', function () {
|
||||
let adapter;
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new InstagramAdapter.constructor();
|
||||
adapter.clientId = 'validClientId';
|
||||
adapter.clientSecret = 'validClientSecret';
|
||||
adapter.redirectUri = 'https://example.com/callback';
|
||||
});
|
||||
|
||||
describe('getAccessTokenFromCode', function () {
|
||||
it('should fetch an access token successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.instagram.com/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode' };
|
||||
const token = await adapter.getAccessTokenFromCode(authData);
|
||||
|
||||
expect(token).toBe('mockAccessToken');
|
||||
});
|
||||
|
||||
it('should throw an error if the response contains an error', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.instagram.com/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
error: 'invalid_grant',
|
||||
error_description: 'Code is invalid',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'invalidCode' };
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
|
||||
'Code is invalid'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserFromAccessToken', function () {
|
||||
it('should fetch user data successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'mockUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'mockAccessToken';
|
||||
const authData = { id: 'mockUserId' };
|
||||
const user = await adapter.getUserFromAccessToken(accessToken, authData);
|
||||
|
||||
expect(user).toEqual({ id: 'mockUserId' });
|
||||
});
|
||||
|
||||
it('should throw an error if user ID does not match authData', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'differentUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'mockAccessToken';
|
||||
const authData = { id: 'mockUserId' };
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken(accessToken, authData)).toBeRejectedWithError(
|
||||
'Instagram auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('InstagramAdapter E2E Test', function () {
|
||||
beforeEach(async function () {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
instagram: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
redirectUri: 'https://example.com/callback',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should log in user successfully with valid code', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.instagram.com/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken123',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'mockUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
id: 'mockUserId',
|
||||
};
|
||||
|
||||
const user = await Parse.User.logInWith('instagram', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle error when access token exchange fails', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.instagram.com/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Invalid code',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'invalidCode',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('instagram', { authData })).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Instagram API request failed.')
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when user data fetch fails', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.instagram.com/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken123',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
id: 'mockUserId',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('instagram', { authData })).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Instagram API request failed.')
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when user data is invalid', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.instagram.com/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken123',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'differentUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
id: 'mockUserId',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('instagram', { authData })).toBeRejectedWithError(
|
||||
'Instagram auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when no code or access token is provided', async function () {
|
||||
mockFetch();
|
||||
|
||||
const authData = {
|
||||
id: 'mockUserId',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('instagram', { authData })).toBeRejectedWithError(
|
||||
'Instagram code is required.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,309 @@
|
||||
const LineAdapter = require('../../../lib/Adapters/Auth/line').default;
|
||||
describe('LineAdapter', function () {
|
||||
let adapter;
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new LineAdapter.constructor();
|
||||
adapter.clientId = 'validClientId';
|
||||
adapter.clientSecret = 'validClientSecret';
|
||||
});
|
||||
|
||||
describe('getAccessTokenFromCode', function () {
|
||||
it('should throw an error if code is missing in authData', async function () {
|
||||
const authData = { redirect_uri: 'http://example.com' };
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
|
||||
'Line auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should fetch an access token successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/oauth2/v2.1/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
redirect_uri: 'http://example.com',
|
||||
};
|
||||
|
||||
const token = await adapter.getAccessTokenFromCode(authData);
|
||||
|
||||
expect(token).toBe('mockAccessToken');
|
||||
});
|
||||
|
||||
it('should throw an error if response is not ok', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/oauth2/v2.1/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Bad Request',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'invalidCode',
|
||||
redirect_uri: 'http://example.com',
|
||||
};
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
|
||||
'Failed to exchange code for token: Bad Request'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if response contains an error object', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/oauth2/v2.1/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
error: 'invalid_grant',
|
||||
error_description: 'Code is invalid',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'invalidCode',
|
||||
redirect_uri: 'http://example.com',
|
||||
};
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
|
||||
'Code is invalid'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserFromAccessToken', function () {
|
||||
it('should fetch user data successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/v2/profile',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
userId: 'mockUserId',
|
||||
displayName: 'mockDisplayName',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'validAccessToken';
|
||||
const user = await adapter.getUserFromAccessToken(accessToken);
|
||||
|
||||
expect(user).toEqual({
|
||||
userId: 'mockUserId',
|
||||
displayName: 'mockDisplayName',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if response is not ok', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/v2/profile',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'invalidAccessToken';
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken(accessToken)).toBeRejectedWithError(
|
||||
'Failed to fetch Line user: Unauthorized'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if user data is invalid', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/v2/profile',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'validAccessToken';
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken(accessToken)).toBeRejectedWithError(
|
||||
'Invalid Line user data received.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('LineAdapter E2E Test', function () {
|
||||
beforeEach(async function () {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
line: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should log in user successfully with valid code', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/oauth2/v2.1/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.line.me/v2/profile',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
userId: 'mockUserId',
|
||||
displayName: 'mockDisplayName',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
redirect_uri: 'http://example.com',
|
||||
};
|
||||
|
||||
const user = await Parse.User.logInWith('line', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle error when token exchange fails', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/oauth2/v2.1/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Invalid code',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'invalidCode',
|
||||
redirect_uri: 'http://example.com',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('line', { authData })).toBeRejectedWithError(
|
||||
'Failed to exchange code for token: Invalid code'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when user data fetch fails', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/oauth2/v2.1/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.line.me/v2/profile',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
redirect_uri: 'http://example.com',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('line', { authData })).toBeRejectedWithError(
|
||||
'Failed to fetch Line user: Unauthorized'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when user data is invalid', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/oauth2/v2.1/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.line.me/v2/profile',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
redirect_uri: 'http://example.com',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('line', { authData })).toBeRejectedWithError(
|
||||
'Invalid Line user data received.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when no code is provided', async function () {
|
||||
mockFetch();
|
||||
|
||||
const authData = {
|
||||
redirect_uri: 'http://example.com',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('line', { authData })).toBeRejectedWithError(
|
||||
'Line code is required.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,312 @@
|
||||
|
||||
const LinkedInAdapter = require('../../../lib/Adapters/Auth/linkedin').default;
|
||||
describe('LinkedInAdapter', function () {
|
||||
let adapter;
|
||||
const validOptions = {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
enableInsecureAuth: false,
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new LinkedInAdapter.constructor();
|
||||
});
|
||||
|
||||
describe('Test configuration errors', function () {
|
||||
it('should throw error for missing options', function () {
|
||||
const invalidOptions = [null, undefined, {}, { clientId: 'validClientId' }];
|
||||
|
||||
for (const options of invalidOptions) {
|
||||
expect(() => {
|
||||
adapter.validateOptions(options);
|
||||
}).toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
it('should validate options successfully with valid parameters', function () {
|
||||
expect(() => {
|
||||
adapter.validateOptions(validOptions);
|
||||
}).not.toThrow();
|
||||
expect(adapter.clientId).toBe(validOptions.clientId);
|
||||
expect(adapter.clientSecret).toBe(validOptions.clientSecret);
|
||||
expect(adapter.enableInsecureAuth).toBe(validOptions.enableInsecureAuth);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test beforeFind', function () {
|
||||
it('should throw error for invalid payload', async function () {
|
||||
adapter.enableInsecureAuth = true;
|
||||
|
||||
const payloads = [{}, { access_token: null }];
|
||||
|
||||
for (const payload of payloads) {
|
||||
await expectAsync(adapter.beforeFind(payload)).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'LinkedIn auth is invalid for this user.')
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should process secure payload and set auth data', async function () {
|
||||
spyOn(adapter, 'getAccessTokenFromCode').and.returnValue(
|
||||
Promise.resolve('validToken')
|
||||
);
|
||||
spyOn(adapter, 'getUserFromAccessToken').and.returnValue(
|
||||
Promise.resolve({ id: 'validUserId' })
|
||||
);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com', is_mobile_sdk: false };
|
||||
|
||||
await adapter.beforeFind(authData);
|
||||
|
||||
expect(authData.access_token).toBe('validToken');
|
||||
expect(authData.id).toBe('validUserId');
|
||||
});
|
||||
|
||||
it('should validate insecure auth and match user id', async function () {
|
||||
adapter.enableInsecureAuth = true;
|
||||
spyOn(adapter, 'getUserFromAccessToken').and.returnValue(
|
||||
Promise.resolve({ id: 'validUserId' })
|
||||
);
|
||||
|
||||
const authData = { access_token: 'validToken', id: 'validUserId', is_mobile_sdk: false };
|
||||
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeResolved();
|
||||
});
|
||||
|
||||
it('should throw error if insecure auth user id does not match', async function () {
|
||||
adapter.enableInsecureAuth = true;
|
||||
spyOn(adapter, 'getUserFromAccessToken').and.returnValue(
|
||||
Promise.resolve({ id: 'invalidUserId' })
|
||||
);
|
||||
|
||||
const authData = { access_token: 'validToken', id: 'validUserId', is_mobile_sdk: false };
|
||||
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeRejectedWith(
|
||||
new Error('LinkedIn auth is invalid for this user.')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test getUserFromAccessToken', function () {
|
||||
it('should fetch user successfully', async function () {
|
||||
global.fetch = jasmine.createSpy().and.returnValue(
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ id: 'validUserId' }),
|
||||
})
|
||||
);
|
||||
|
||||
const user = await adapter.getUserFromAccessToken('validToken', false);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith('https://api.linkedin.com/v2/me', {
|
||||
headers: {
|
||||
Authorization: `Bearer validToken`,
|
||||
'x-li-format': 'json',
|
||||
'x-li-src': undefined,
|
||||
},
|
||||
});
|
||||
expect(user).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
|
||||
it('should throw error for invalid response', async function () {
|
||||
global.fetch = jasmine.createSpy().and.returnValue(
|
||||
Promise.resolve({ ok: false })
|
||||
);
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken('invalidToken', false)).toBeRejectedWith(
|
||||
new Error('LinkedIn API request failed.')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test getAccessTokenFromCode', function () {
|
||||
it('should fetch token successfully', async function () {
|
||||
global.fetch = jasmine.createSpy().and.returnValue(
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validToken' }),
|
||||
})
|
||||
);
|
||||
|
||||
const tokenResponse = await adapter.getAccessTokenFromCode('validCode', 'http://example.com');
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith('https://www.linkedin.com/oauth/v2/accessToken', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: jasmine.any(URLSearchParams),
|
||||
});
|
||||
expect(tokenResponse).toEqual('validToken');
|
||||
});
|
||||
|
||||
it('should throw error for invalid response', async function () {
|
||||
global.fetch = jasmine.createSpy().and.returnValue(
|
||||
Promise.resolve({ ok: false })
|
||||
);
|
||||
|
||||
await expectAsync(
|
||||
adapter.getAccessTokenFromCode('invalidCode', 'http://example.com')
|
||||
).toBeRejectedWith(new Error('LinkedIn API request failed.'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test validate methods', function () {
|
||||
const authData = { id: 'validUserId', access_token: 'validToken' };
|
||||
|
||||
it('validateLogin should return user id', function () {
|
||||
const result = adapter.validateLogin(authData);
|
||||
expect(result).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
|
||||
it('validateSetUp should return user id', function () {
|
||||
const result = adapter.validateSetUp(authData);
|
||||
expect(result).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
|
||||
it('validateUpdate should return user id', function () {
|
||||
const result = adapter.validateUpdate(authData);
|
||||
expect(result).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
|
||||
it('afterFind should return user id', function () {
|
||||
const result = adapter.afterFind(authData);
|
||||
expect(result).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('LinkedInAdapter E2E Test', function () {
|
||||
beforeEach(async function () {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
linkedin: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should log in user using LinkedIn adapter successfully (secure)', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://www.linkedin.com/oauth/v2/accessToken',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.linkedin.com/v2/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'mockUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'https://example.com/callback' };
|
||||
const user = await Parse.User.logInWith('linkedin', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://www.linkedin.com/oauth/v2/accessToken',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://api.linkedin.com/v2/me',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when LinkedIn returns invalid user data', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://www.linkedin.com/oauth/v2/accessToken',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.linkedin.com/v2/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'https://example.com/callback' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('linkedin', { authData })).toBeRejectedWithError(
|
||||
'LinkedIn API request failed.'
|
||||
);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://www.linkedin.com/oauth/v2/accessToken',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://api.linkedin.com/v2/me',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('secure does not support insecure payload if not enabled', async function () {
|
||||
mockFetch();
|
||||
const authData = { id: 'mockUserId', access_token: 'mockAccessToken123' };
|
||||
await expectAsync(Parse.User.logInWith('linkedin', { authData })).toBeRejectedWithError(
|
||||
'LinkedIn code is required.'
|
||||
);
|
||||
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('insecure mode supports insecure payload if enabled', async function () {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
linkedin: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
enableInsecureAuth: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.linkedin.com/v2/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'mockUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { id: 'mockUserId', access_token: 'mockAccessToken123' };
|
||||
const user = await Parse.User.logInWith('linkedin', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://api.linkedin.com/v2/me',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,307 @@
|
||||
const MicrosoftAdapter = require('../../../lib/Adapters/Auth/microsoft').default;
|
||||
|
||||
describe('MicrosoftAdapter', function () {
|
||||
let adapter;
|
||||
const validOptions = {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
enableInsecureAuth: false,
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new MicrosoftAdapter.constructor();
|
||||
});
|
||||
|
||||
describe('Test configuration errors', function () {
|
||||
it('should throw error for missing options', function () {
|
||||
const invalidOptions = [null, undefined, {}, { clientId: 'validClientId' }];
|
||||
|
||||
for (const options of invalidOptions) {
|
||||
expect(() => {
|
||||
adapter.validateOptions(options);
|
||||
}).toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
it('should validate options successfully with valid parameters', function () {
|
||||
expect(() => {
|
||||
adapter.validateOptions(validOptions);
|
||||
}).not.toThrow();
|
||||
expect(adapter.clientId).toBe(validOptions.clientId);
|
||||
expect(adapter.clientSecret).toBe(validOptions.clientSecret);
|
||||
expect(adapter.enableInsecureAuth).toBe(validOptions.enableInsecureAuth);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test getUserFromAccessToken', function () {
|
||||
it('should fetch user successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.microsoft.com/v1.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ id: 'validUserId' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const user = await adapter.getUserFromAccessToken('validToken');
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith('https://graph.microsoft.com/v1.0/me', {
|
||||
headers: {
|
||||
Authorization: 'Bearer validToken',
|
||||
},
|
||||
method: 'GET',
|
||||
});
|
||||
expect(user).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
|
||||
it('should throw error for invalid response', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.microsoft.com/v1.0/me',
|
||||
method: 'GET',
|
||||
response: { ok: false },
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken('invalidToken')).toBeRejectedWith(
|
||||
new Error('Microsoft API request failed.')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test getAccessTokenFromCode', function () {
|
||||
it('should fetch token successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validToken' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com' };
|
||||
const token = await adapter.getAccessTokenFromCode(authData);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: jasmine.any(URLSearchParams),
|
||||
});
|
||||
expect(token).toEqual('validToken');
|
||||
});
|
||||
|
||||
it('should throw error for invalid response', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
method: 'POST',
|
||||
response: { ok: false },
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'invalidCode', redirect_uri: 'http://example.com' };
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWith(
|
||||
new Error('Microsoft API request failed.')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test secure authentication flow', function () {
|
||||
it('should exchange code for access token and fetch user data', async function () {
|
||||
spyOn(adapter, 'getAccessTokenFromCode').and.returnValue(Promise.resolve('validToken'));
|
||||
spyOn(adapter, 'getUserFromAccessToken').and.returnValue(Promise.resolve({ id: 'validUserId' }));
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com' };
|
||||
await adapter.beforeFind(authData);
|
||||
|
||||
expect(authData.access_token).toBe('validToken');
|
||||
expect(authData.id).toBe('validUserId');
|
||||
});
|
||||
|
||||
it('should throw error if user data cannot be fetched', async function () {
|
||||
spyOn(adapter, 'getAccessTokenFromCode').and.returnValue(Promise.resolve('validToken'));
|
||||
spyOn(adapter, 'getUserFromAccessToken').and.throwError('Microsoft API request failed.');
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com' };
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeRejectedWith(
|
||||
new Error('Microsoft API request failed.')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test insecure authentication flow', function () {
|
||||
beforeEach(function () {
|
||||
adapter.enableInsecureAuth = true;
|
||||
});
|
||||
|
||||
it('should validate insecure auth and match user id', async function () {
|
||||
spyOn(adapter, 'getUserFromAccessToken').and.returnValue(
|
||||
Promise.resolve({ id: 'validUserId' })
|
||||
);
|
||||
|
||||
const authData = { access_token: 'validToken', id: 'validUserId' };
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeResolved();
|
||||
});
|
||||
|
||||
it('should throw error if insecure auth user id does not match', async function () {
|
||||
spyOn(adapter, 'getUserFromAccessToken').and.returnValue(
|
||||
Promise.resolve({ id: 'invalidUserId' })
|
||||
);
|
||||
|
||||
const authData = { access_token: 'validToken', id: 'validUserId' };
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeRejectedWith(
|
||||
new Error('Microsoft auth is invalid for this user.')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MicrosoftAdapter E2E Tests', () => {
|
||||
beforeEach(async () => {
|
||||
// Simulate reconfiguring the server with Microsoft auth options
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
microsoft: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
enableInsecureAuth: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should authenticate user successfully using MicrosoftAdapter', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validAccessToken' }),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://graph.microsoft.com/v1.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ id: 'user123' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
|
||||
const user = await Parse.User.logInWith('microsoft', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle invalid code error gracefully', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
method: 'POST',
|
||||
response: { ok: false, statusText: 'Invalid code' },
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'invalidCode', redirect_uri: 'http://example.com/callback' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('microsoft', { authData })).toBeRejectedWithError(
|
||||
'Microsoft API request failed.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when fetching user data fails', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validAccessToken' }),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://graph.microsoft.com/v1.0/me',
|
||||
method: 'GET',
|
||||
response: { ok: false, statusText: 'Unauthorized' },
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('microsoft', { authData })).toBeRejectedWithError(
|
||||
'Microsoft API request failed.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow insecure auth when enabled', async () => {
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.microsoft.com/v1.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
id: 'user123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
microsoft: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
enableInsecureAuth: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const authData = { access_token: 'validAccessToken', id: 'user123' };
|
||||
const user = await Parse.User.logInWith('microsoft', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should reject insecure auth when user id does not match', async () => {
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.microsoft.com/v1.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
id: 'incorrectUser',
|
||||
}),
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
microsoft: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
enableInsecureAuth: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const authData = { access_token: 'validAccessToken', id: 'incorrectUserId' };
|
||||
await expectAsync(Parse.User.logInWith('microsoft', { authData })).toBeRejectedWithError(
|
||||
'Microsoft auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,305 @@
|
||||
const OAuth2Adapter = require('../../../lib/Adapters/Auth/oauth2').default;
|
||||
|
||||
describe('OAuth2Adapter', () => {
|
||||
let adapter;
|
||||
|
||||
const validOptions = {
|
||||
tokenIntrospectionEndpointUrl: 'https://provider.com/introspect',
|
||||
useridField: 'sub',
|
||||
appidField: 'aud',
|
||||
appIds: ['valid-app-id'],
|
||||
authorizationHeader: 'Bearer validAuthToken',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
adapter = new OAuth2Adapter.constructor();
|
||||
adapter.validateOptions(validOptions);
|
||||
});
|
||||
|
||||
describe('validateAppId', () => {
|
||||
it('should validate app ID successfully', async () => {
|
||||
const authData = { access_token: 'validAccessToken' };
|
||||
const mockResponse = {
|
||||
[validOptions.appidField]: 'valid-app-id',
|
||||
};
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: validOptions.tokenIntrospectionEndpointUrl,
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(
|
||||
adapter.validateAppId(validOptions.appIds, authData, validOptions)
|
||||
).toBeResolved();
|
||||
});
|
||||
|
||||
it('should throw an error if app ID is invalid', async () => {
|
||||
const authData = { access_token: 'validAccessToken' };
|
||||
const mockResponse = {
|
||||
[validOptions.appidField]: 'invalid-app-id',
|
||||
};
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: validOptions.tokenIntrospectionEndpointUrl,
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(
|
||||
adapter.validateAppId(validOptions.appIds, authData, validOptions)
|
||||
).toBeRejectedWithError('OAuth2: Invalid app ID.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateAuthData', () => {
|
||||
it('should validate auth data successfully', async () => {
|
||||
const authData = { id: 'user-id', access_token: 'validAccessToken' };
|
||||
const mockResponse = {
|
||||
active: true,
|
||||
[validOptions.useridField]: 'user-id',
|
||||
};
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: validOptions.tokenIntrospectionEndpointUrl,
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(
|
||||
adapter.validateAuthData(authData, null, validOptions)
|
||||
).toBeResolvedTo({});
|
||||
});
|
||||
|
||||
it('should throw an error if the token is inactive', async () => {
|
||||
const authData = { id: 'user-id', access_token: 'validAccessToken' };
|
||||
const mockResponse = { active: false };
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: validOptions.tokenIntrospectionEndpointUrl,
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(
|
||||
adapter.validateAuthData(authData, null, validOptions)
|
||||
).toBeRejectedWith(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 access token is invalid for this user.'));
|
||||
});
|
||||
|
||||
it('should throw an error if user ID does not match', async () => {
|
||||
const authData = { id: 'user-id', access_token: 'validAccessToken' };
|
||||
const mockResponse = {
|
||||
active: true,
|
||||
[validOptions.useridField]: 'different-user-id',
|
||||
};
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: validOptions.tokenIntrospectionEndpointUrl,
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(
|
||||
adapter.validateAuthData(authData, null, validOptions)
|
||||
).toBeRejectedWithError('OAuth2 access token is invalid for this user.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('requestTokenInfo', () => {
|
||||
it('should fetch token info successfully', async () => {
|
||||
const mockResponse = { active: true };
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: validOptions.tokenIntrospectionEndpointUrl,
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await adapter.requestTokenInfo(
|
||||
'validAccessToken',
|
||||
validOptions
|
||||
);
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('should throw an error if the introspection endpoint URL is missing', async () => {
|
||||
const options = { ...validOptions, tokenIntrospectionEndpointUrl: null };
|
||||
|
||||
expect(
|
||||
() => adapter.validateOptions(options)
|
||||
).toThrow(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 token introspection endpoint URL is missing.'));
|
||||
});
|
||||
|
||||
it('should throw an error if the response is not ok', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: validOptions.tokenIntrospectionEndpointUrl,
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Bad Request',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(
|
||||
adapter.requestTokenInfo('invalidAccessToken')
|
||||
).toBeRejectedWithError('OAuth2 token introspection request failed.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('OAuth2Adapter E2E Tests', () => {
|
||||
beforeEach(async () => {
|
||||
// Simulate reconfiguring the server with OAuth2 auth options
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
mockOauth: {
|
||||
tokenIntrospectionEndpointUrl: 'https://provider.com/introspect',
|
||||
useridField: 'sub',
|
||||
appidField: 'aud',
|
||||
appIds: ['valid-app-id'],
|
||||
authorizationHeader: 'Bearer validAuthToken',
|
||||
oauth2: true
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should validate and authenticate user successfully', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://provider.com/introspect',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
active: true,
|
||||
sub: 'user123',
|
||||
aud: 'valid-app-id',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { access_token: 'validAccessToken', id: 'user123' };
|
||||
const user = await Parse.User.logInWith('mockOauth', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
expect(user.get('authData').mockOauth.id).toEqual('user123');
|
||||
});
|
||||
|
||||
it('should reject authentication for inactive token', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://provider.com/introspect',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ active: false, aud: ['valid-app-id'] }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { access_token: 'inactiveToken', id: 'user123' };
|
||||
await expectAsync(Parse.User.logInWith('mockOauth', { authData })).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 access token is invalid for this user.')
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject authentication for mismatched user ID', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://provider.com/introspect',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
active: true,
|
||||
sub: 'different-user',
|
||||
aud: 'valid-app-id',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { access_token: 'validAccessToken', id: 'user123' };
|
||||
await expectAsync(Parse.User.logInWith('mockOauth', { authData })).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 access token is invalid for this user.')
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject authentication for invalid app ID', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://provider.com/introspect',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
active: true,
|
||||
sub: 'user123',
|
||||
aud: 'invalid-app-id',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { access_token: 'validAccessToken', id: 'user123' };
|
||||
await expectAsync(Parse.User.logInWith('mockOauth', { authData })).toBeRejectedWithError(
|
||||
'OAuth2: Invalid app ID.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when token introspection endpoint is missing', async () => {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
mockOauth: {
|
||||
tokenIntrospectionEndpointUrl: null,
|
||||
useridField: 'sub',
|
||||
appidField: 'aud',
|
||||
appIds: ['valid-app-id'],
|
||||
authorizationHeader: 'Bearer validAuthToken',
|
||||
oauth2: true
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const authData = { access_token: 'validAccessToken', id: 'user123' };
|
||||
await expectAsync(Parse.User.logInWith('mockOauth', { authData })).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 token introspection endpoint URL is missing.')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,252 @@
|
||||
const QqAdapter = require('../../../lib/Adapters/Auth/qq').default;
|
||||
|
||||
describe('QqAdapter', () => {
|
||||
let adapter;
|
||||
|
||||
beforeEach(() => {
|
||||
adapter = new QqAdapter.constructor();
|
||||
});
|
||||
|
||||
describe('getUserFromAccessToken', () => {
|
||||
it('should fetch user data successfully', async () => {
|
||||
const mockResponse = `callback({"client_id":"validAppId","openid":"user123"})`;
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
text: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await adapter.getUserFromAccessToken('validAccessToken');
|
||||
|
||||
expect(result).toEqual({ client_id: 'validAppId', openid: 'user123' });
|
||||
});
|
||||
|
||||
it('should throw an error if the API request fails', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(
|
||||
adapter.getUserFromAccessToken('invalidAccessToken')
|
||||
).toBeRejectedWithError('qq API request failed.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAccessTokenFromCode', () => {
|
||||
it('should fetch access token successfully', async () => {
|
||||
const mockResponse = `callback({"access_token":"validAccessToken","expires_in":3600,"refresh_token":"refreshToken"})`;
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/token',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
text: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await adapter.getAccessTokenFromCode({
|
||||
code: 'validCode',
|
||||
redirect_uri: 'https://your-redirect-uri.com/callback',
|
||||
});
|
||||
|
||||
expect(result).toBe('validAccessToken');
|
||||
});
|
||||
|
||||
it('should throw an error if the API request fails', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/token',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Bad Request',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(
|
||||
adapter.getAccessTokenFromCode({
|
||||
code: 'invalidCode',
|
||||
redirect_uri: 'https://your-redirect-uri.com/callback',
|
||||
})
|
||||
).toBeRejectedWithError('qq API request failed.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseResponseData', () => {
|
||||
it('should parse valid callback response data', () => {
|
||||
const response = `callback({"key":"value"})`;
|
||||
const result = adapter.parseResponseData(response);
|
||||
|
||||
expect(result).toEqual({ key: 'value' });
|
||||
});
|
||||
|
||||
it('should throw an error if the response data is invalid', () => {
|
||||
const response = 'invalid response';
|
||||
|
||||
expect(() => adapter.parseResponseData(response)).toThrowError(
|
||||
'qq auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('QqAdapter E2E Test', () => {
|
||||
beforeEach(async () => {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
qq: {
|
||||
clientId: 'validAppId',
|
||||
clientSecret: 'validAppSecret',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should log in user using Qq adapter successfully', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/token',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
text: () =>
|
||||
Promise.resolve(
|
||||
`callback({"access_token":"mockAccessToken","expires_in":3600})`
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
text: () =>
|
||||
Promise.resolve(
|
||||
`callback({"client_id":"validAppId","openid":"user123"})`
|
||||
),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'https://your-redirect-uri.com/callback' };
|
||||
const user = await Parse.User.logInWith('qq', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle error when Qq returns invalid code', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/token',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Invalid code',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'invalidCode', redirect_uri: 'https://your-redirect-uri.com/callback' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('qq', { authData })).toBeRejectedWithError(
|
||||
'qq API request failed.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when Qq returns invalid user data', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/token',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
text: () =>
|
||||
Promise.resolve(
|
||||
`callback({"access_token":"mockAccessToken","expires_in":3600})`
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'https://your-redirect-uri.com/callback' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('qq', { authData })).toBeRejectedWithError(
|
||||
'qq API request failed.'
|
||||
);
|
||||
});
|
||||
|
||||
it('e2e secure does not support insecure payload', async () => {
|
||||
mockFetch();
|
||||
const authData = { id: 'mockUserId', access_token: 'mockAccessToken' };
|
||||
await expectAsync(Parse.User.logInWith('qq', { authData })).toBeRejectedWithError(
|
||||
'qq code is required.'
|
||||
);
|
||||
});
|
||||
|
||||
it('e2e insecure does support secure payload', async () => {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
qq: {
|
||||
appId: 'validAppId',
|
||||
appSecret: 'validAppSecret',
|
||||
enableInsecureAuth: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/token',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
text: () =>
|
||||
Promise.resolve(
|
||||
`callback({"access_token":"mockAccessToken","expires_in":3600})`
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
text: () =>
|
||||
Promise.resolve(
|
||||
`callback({"client_id":"validAppId","openid":"user123"})`
|
||||
),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'https://your-redirect-uri.com/callback' };
|
||||
const user = await Parse.User.logInWith('qq', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
const SpotifyAdapter = require('../../../lib/Adapters/Auth/spotify').default;
|
||||
|
||||
describe('SpotifyAdapter', () => {
|
||||
let adapter;
|
||||
|
||||
beforeEach(() => {
|
||||
adapter = new SpotifyAdapter.constructor();
|
||||
});
|
||||
|
||||
describe('getUserFromAccessToken', () => {
|
||||
it('should fetch user data successfully', async () => {
|
||||
const mockResponse = {
|
||||
id: 'spotifyUser123',
|
||||
};
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.spotify.com/v1/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await adapter.getUserFromAccessToken('validAccessToken');
|
||||
|
||||
expect(result).toEqual({ id: 'spotifyUser123' });
|
||||
});
|
||||
|
||||
it('should throw an error if the API request fails', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.spotify.com/v1/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken('invalidAccessToken')).toBeRejectedWithError(
|
||||
'Spotify API request failed.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAccessTokenFromCode', () => {
|
||||
it('should fetch access token successfully', async () => {
|
||||
const mockResponse = {
|
||||
access_token: 'validAccessToken',
|
||||
expires_in: 3600,
|
||||
refresh_token: 'refreshToken',
|
||||
};
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://accounts.spotify.com/api/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
redirect_uri: 'https://your-redirect-uri.com/callback',
|
||||
code_verifier: 'validCodeVerifier',
|
||||
};
|
||||
|
||||
const result = await adapter.getAccessTokenFromCode(authData);
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('should throw an error if authData is missing required fields', async () => {
|
||||
const authData = {
|
||||
redirect_uri: 'https://your-redirect-uri.com/callback',
|
||||
};
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
|
||||
'Spotify auth configuration authData.code and/or authData.redirect_uri and/or authData.code_verifier.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if the API request fails', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://accounts.spotify.com/api/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Bad Request',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'invalidCode',
|
||||
redirect_uri: 'https://your-redirect-uri.com/callback',
|
||||
code_verifier: 'invalidCodeVerifier',
|
||||
};
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
|
||||
'Spotify API request failed.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
const TwitterAuthAdapter = require('../../../lib/Adapters/Auth/twitter').default;
|
||||
|
||||
describe('TwitterAuthAdapter', function () {
|
||||
let adapter;
|
||||
const validOptions = {
|
||||
consumer_key: 'validConsumerKey',
|
||||
consumer_secret: 'validConsumerSecret',
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new TwitterAuthAdapter.constructor();
|
||||
});
|
||||
|
||||
describe('Test configuration errors', function () {
|
||||
it('should throw an error when options are missing', function () {
|
||||
expect(() => adapter.validateOptions()).toThrowError('Twitter auth options are required.');
|
||||
});
|
||||
|
||||
it('should throw an error when consumer_key and consumer_secret are missing for secure auth', function () {
|
||||
const options = { enableInsecureAuth: false };
|
||||
expect(() => adapter.validateOptions(options)).toThrowError(
|
||||
'Consumer key and secret are required for secure Twitter auth.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should not throw an error when valid options are provided', function () {
|
||||
expect(() => adapter.validateOptions(validOptions)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Validate Insecure Auth', function () {
|
||||
it('should throw an error if oauth_token or oauth_token_secret are missing', async function () {
|
||||
const authData = { oauth_token: 'validToken' }; // Missing oauth_token_secret
|
||||
await expectAsync(adapter.validateInsecureAuth(authData, validOptions)).toBeRejectedWithError(
|
||||
'Twitter insecure auth requires oauth_token and oauth_token_secret.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should validate insecure auth successfully when data matches', async function () {
|
||||
spyOn(adapter, 'request').and.returnValue(
|
||||
Promise.resolve({
|
||||
json: () => Promise.resolve({ id: 'validUserId' }),
|
||||
})
|
||||
);
|
||||
|
||||
const authData = {
|
||||
id: 'validUserId',
|
||||
oauth_token: 'validToken',
|
||||
oauth_token_secret: 'validSecret',
|
||||
};
|
||||
await expectAsync(adapter.validateInsecureAuth(authData, validOptions)).toBeResolved();
|
||||
});
|
||||
|
||||
it('should throw an error when user ID does not match', async function () {
|
||||
spyOn(adapter, 'request').and.returnValue(
|
||||
Promise.resolve({
|
||||
json: () => Promise.resolve({ id: 'invalidUserId' }),
|
||||
})
|
||||
);
|
||||
|
||||
const authData = {
|
||||
id: 'validUserId',
|
||||
oauth_token: 'validToken',
|
||||
oauth_token_secret: 'validSecret',
|
||||
};
|
||||
await expectAsync(adapter.validateInsecureAuth(authData, validOptions)).toBeRejectedWithError(
|
||||
'Twitter auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('End-to-End Tests', function () {
|
||||
beforeEach(async function () {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
twitter: validOptions,
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
it('should authenticate user successfully using validateAuthData', async function () {
|
||||
spyOn(adapter, 'exchangeAccessToken').and.returnValue(
|
||||
Promise.resolve({ oauth_token: 'validToken', user_id: 'validUserId' })
|
||||
);
|
||||
|
||||
const authData = {
|
||||
oauth_token: 'validToken',
|
||||
oauth_verifier: 'validVerifier',
|
||||
};
|
||||
await expectAsync(adapter.validateAuthData(authData, validOptions)).toBeResolved();
|
||||
expect(authData.id).toBe('validUserId');
|
||||
expect(authData.auth_token).toBe('validToken');
|
||||
});
|
||||
|
||||
it('should handle multiple configurations and validate successfully', async function () {
|
||||
const authData = {
|
||||
consumer_key: 'validConsumerKey',
|
||||
oauth_token: 'validToken',
|
||||
oauth_token_secret: 'validSecret',
|
||||
};
|
||||
|
||||
const optionsArray = [
|
||||
{ consumer_key: 'invalidKey', consumer_secret: 'invalidSecret' },
|
||||
validOptions,
|
||||
];
|
||||
|
||||
const selectedOption = adapter.handleMultipleConfigurations(authData, optionsArray);
|
||||
expect(selectedOption).toEqual(validOptions);
|
||||
});
|
||||
|
||||
it('should throw an error when no matching configuration is found', function () {
|
||||
const authData = { consumer_key: 'missingKey' };
|
||||
const optionsArray = [validOptions];
|
||||
|
||||
expect(() => adapter.handleMultipleConfigurations(authData, optionsArray)).toThrowError(
|
||||
'Twitter auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,234 @@
|
||||
const WeChatAdapter = require('../../../lib/Adapters/Auth/wechat').default;
|
||||
|
||||
describe('WeChatAdapter', function () {
|
||||
let adapter;
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new WeChatAdapter.constructor();
|
||||
});
|
||||
|
||||
describe('Test getUserFromAccessToken', function () {
|
||||
it('should fetch user successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/auth?access_token=validToken&openid=validOpenId',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ errcode: 0, id: 'validUserId' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const user = await adapter.getUserFromAccessToken('validToken', { id: 'validOpenId' });
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://api.weixin.qq.com/sns/auth?access_token=validToken&openid=validOpenId'
|
||||
);
|
||||
expect(user).toEqual({ errcode: 0, id: 'validUserId' });
|
||||
});
|
||||
|
||||
it('should throw error for invalid response', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/auth?access_token=invalidToken&openid=undefined',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
json: () => Promise.resolve({ errcode: 40013, errmsg: 'Invalid token' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken('invalidToken', 'invalidOpenId')).toBeRejectedWith(
|
||||
jasmine.objectContaining({ message: 'WeChat auth is invalid for this user.' })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test getAccessTokenFromCode', function () {
|
||||
it('should fetch access token successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=validCode&grant_type=authorization_code',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validToken', errcode: 0 }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
adapter.validateOptions({ clientId: 'validAppId', clientSecret: 'validAppSecret' });
|
||||
const authData = { code: 'validCode' };
|
||||
const token = await adapter.getAccessTokenFromCode(authData);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=validCode&grant_type=authorization_code'
|
||||
);
|
||||
expect(token).toEqual('validToken');
|
||||
});
|
||||
|
||||
it('should throw error for invalid response', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=invalidCode&grant_type=authorization_code',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
json: () => Promise.resolve({ errcode: 40029, errmsg: 'Invalid code' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
adapter.validateOptions({ clientId: 'validAppId', clientSecret: 'validAppSecret' });
|
||||
|
||||
const authData = { code: 'invalidCode' };
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWith(
|
||||
jasmine.objectContaining({ message: 'WeChat auth is invalid for this user.' })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('WeChatAdapter E2E Tests', function () {
|
||||
beforeEach(async () => {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
wechat: {
|
||||
clientId: 'validAppId',
|
||||
clientSecret: 'validAppSecret',
|
||||
enableInsecureAuth: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should authenticate user successfully using WeChatAdapter', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=validCode&grant_type=authorization_code',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validAccessToken', openid: 'user123', errcode: 0 }),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/auth?access_token=validAccessToken&openid=user123',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ errcode: 0, id: 'user123' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
|
||||
const user = await Parse.User.logInWith('wechat', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle invalid code error gracefully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=invalidCode&grant_type=authorization_code',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
json: () => Promise.resolve({ errcode: 40029, errmsg: 'Invalid code' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'invalidCode', redirect_uri: 'http://example.com/callback' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('wechat', { authData })).toBeRejectedWith(
|
||||
jasmine.objectContaining({ message: 'WeChat auth is invalid for this user.' })
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when fetching user data fails', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=validCode&grant_type=authorization_code',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validAccessToken', openid: 'user123', errcode: 0 }),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/auth?access_token=validAccessToken&openid=user123',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
json: () => Promise.resolve({ errcode: 40013, errmsg: 'Invalid token' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('wechat', { authData })).toBeRejectedWith(
|
||||
jasmine.objectContaining({ message: 'WeChat auth is invalid for this user.' })
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow insecure auth when enabled', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/auth?access_token=validAccessToken&openid=user123',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ errcode: 0, id: 'user123' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
wechat: {
|
||||
appId: 'validAppId',
|
||||
appSecret: 'validAppSecret',
|
||||
enableInsecureAuth: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const authData = { access_token: 'validAccessToken', id: 'user123' };
|
||||
const user = await Parse.User.logInWith('wechat', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should reject insecure auth when user id does not match', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/auth?access_token=validAccessToken&openid=incorrectUserId',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ errcode: 0, id: 'incorrectUser' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
wechat: {
|
||||
appId: 'validAppId',
|
||||
appSecret: 'validAppSecret',
|
||||
enableInsecureAuth: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const authData = { access_token: 'validAccessToken', id: 'incorrectUserId' };
|
||||
await expectAsync(Parse.User.logInWith('wechat', { authData })).toBeRejectedWith(
|
||||
jasmine.objectContaining({ message: 'WeChat auth is invalid for this user.' })
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,204 @@
|
||||
const WeiboAdapter = require('../../../lib/Adapters/Auth/weibo').default;
|
||||
|
||||
describe('WeiboAdapter', function () {
|
||||
let adapter;
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new WeiboAdapter.constructor();
|
||||
});
|
||||
|
||||
describe('Test configuration errors', function () {
|
||||
it('should throw error if code or redirect_uri is missing', async function () {
|
||||
const invalidAuthData = [
|
||||
{},
|
||||
{ code: 'validCode' },
|
||||
{ redirect_uri: 'http://example.com/callback' },
|
||||
];
|
||||
|
||||
for (const authData of invalidAuthData) {
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWith(
|
||||
jasmine.objectContaining({
|
||||
message: 'Weibo auth requires code and redirect_uri to be sent.',
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test getUserFromAccessToken', function () {
|
||||
it('should fetch user successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weibo.com/oauth2/get_token_info',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ uid: 'validUserId' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { id: 'validUserId' };
|
||||
const user = await adapter.getUserFromAccessToken('validToken', authData);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://api.weibo.com/oauth2/get_token_info',
|
||||
jasmine.objectContaining({
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
})
|
||||
);
|
||||
expect(user).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
|
||||
it('should throw error for invalid response', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weibo.com/oauth2/get_token_info',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
json: () => Promise.resolve({}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { id: 'invalidUserId' };
|
||||
await expectAsync(adapter.getUserFromAccessToken('invalidToken', authData)).toBeRejectedWith(
|
||||
jasmine.objectContaining({
|
||||
message: 'Weibo auth is invalid for this user.',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test getAccessTokenFromCode', function () {
|
||||
it('should fetch access token successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weibo.com/oauth2/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validToken', uid: 'validUserId' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
|
||||
const token = await adapter.getAccessTokenFromCode(authData);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://api.weibo.com/oauth2/access_token',
|
||||
jasmine.objectContaining({
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
})
|
||||
);
|
||||
expect(token).toEqual('validToken');
|
||||
});
|
||||
|
||||
it('should throw error for invalid response', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weibo.com/oauth2/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
json: () => Promise.resolve({ errcode: 40029 }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'invalidCode', redirect_uri: 'http://example.com/callback' };
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWith(
|
||||
jasmine.objectContaining({
|
||||
message: 'Weibo auth is invalid for this user.',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('WeiboAdapter E2E Tests', function () {
|
||||
beforeEach(async () => {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
weibo: {
|
||||
clientId: 'validAppId',
|
||||
clientSecret: 'validAppSecret',
|
||||
},
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should authenticate user successfully using WeiboAdapter', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weibo.com/oauth2/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validAccessToken', uid: 'user123' }),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.weibo.com/oauth2/get_token_info',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ uid: 'user123' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
|
||||
const user = await Parse.User.logInWith('weibo', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle invalid code error gracefully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weibo.com/oauth2/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
json: () => Promise.resolve({ errcode: 40029 }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'invalidCode', redirect_uri: 'http://example.com/callback' };
|
||||
await expectAsync(Parse.User.logInWith('weibo', { authData })).toBeRejectedWith(
|
||||
jasmine.objectContaining({ message: 'Weibo auth is invalid for this user.' })
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when fetching user data fails', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weibo.com/oauth2/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validAccessToken', uid: 'user123' }),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.weibo.com/oauth2/get_token_info',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
json: () => Promise.resolve({}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
|
||||
await expectAsync(Parse.User.logInWith('weibo', { authData })).toBeRejectedWith(
|
||||
jasmine.objectContaining({ message: 'Weibo auth is invalid for this user.' })
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -317,7 +317,7 @@ describe('AudiencesRouter', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('should support legacy parse.com audience fields', done => {
|
||||
it_id('af1111b5-3251-4b40-8f06-fb0fc624fa91')(it_exclude_dbs(['postgres']))('should support legacy parse.com audience fields', done => {
|
||||
const database = Config.get(Parse.applicationId).database.adapter.database;
|
||||
const now = new Date();
|
||||
Parse._request(
|
||||
|
||||
@@ -106,6 +106,8 @@ describe('Auth', () => {
|
||||
updatedAt: updatedAt.toISOString(),
|
||||
}
|
||||
);
|
||||
Parse.Server.cacheController.clear();
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await session.fetch();
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await session.fetch();
|
||||
|
||||
@@ -3,99 +3,8 @@ const Config = require('../lib/Config');
|
||||
const defaultColumns = require('../lib/Controllers/SchemaController').defaultColumns;
|
||||
const authenticationLoader = require('../lib/Adapters/Auth');
|
||||
const path = require('path');
|
||||
const responses = {
|
||||
gpgames: { playerId: 'userId' },
|
||||
instagram: { id: 'userId' },
|
||||
janrainengage: { stat: 'ok', profile: { identifier: 'userId' } },
|
||||
janraincapture: { stat: 'ok', result: 'userId' },
|
||||
line: { userId: 'userId' },
|
||||
vkontakte: { response: [{ id: 'userId' }] },
|
||||
google: { sub: 'userId' },
|
||||
wechat: { errcode: 0 },
|
||||
weibo: { uid: 'userId' },
|
||||
qq: 'callback( {"openid":"userId"} );', // yes it's like that, run eval in the client :P
|
||||
phantauth: { sub: 'userId' },
|
||||
microsoft: { id: 'userId', mail: 'userMail' },
|
||||
};
|
||||
|
||||
describe('AuthenticationProviders', function () {
|
||||
[
|
||||
'apple',
|
||||
'gcenter',
|
||||
'gpgames',
|
||||
'facebook',
|
||||
'github',
|
||||
'instagram',
|
||||
'google',
|
||||
'linkedin',
|
||||
'meetup',
|
||||
'twitter',
|
||||
'janrainengage',
|
||||
'janraincapture',
|
||||
'line',
|
||||
'vkontakte',
|
||||
'qq',
|
||||
'spotify',
|
||||
'wechat',
|
||||
'weibo',
|
||||
'phantauth',
|
||||
'microsoft',
|
||||
'keycloak',
|
||||
].map(function (providerName) {
|
||||
it('Should validate structure of ' + providerName, done => {
|
||||
const provider = require('../lib/Adapters/Auth/' + providerName);
|
||||
jequal(typeof provider.validateAuthData, 'function');
|
||||
jequal(typeof provider.validateAppId, 'function');
|
||||
const validateAuthDataPromise = provider.validateAuthData({}, {});
|
||||
const validateAppIdPromise = provider.validateAppId('app', 'key', {});
|
||||
jequal(validateAuthDataPromise.constructor, Promise.prototype.constructor);
|
||||
jequal(validateAppIdPromise.constructor, Promise.prototype.constructor);
|
||||
validateAuthDataPromise.then(
|
||||
() => {},
|
||||
() => {}
|
||||
);
|
||||
validateAppIdPromise.then(
|
||||
() => {},
|
||||
() => {}
|
||||
);
|
||||
done();
|
||||
});
|
||||
|
||||
it(`should provide the right responses for adapter ${providerName}`, async () => {
|
||||
const noResponse = ['twitter', 'apple', 'gcenter', 'google', 'keycloak'];
|
||||
if (noResponse.includes(providerName)) {
|
||||
return;
|
||||
}
|
||||
spyOn(require('../lib/Adapters/Auth/httpsRequest'), 'get').and.callFake(options => {
|
||||
if (
|
||||
options ===
|
||||
'https://oauth.vk.com/access_token?client_id=appId&client_secret=appSecret&v=5.123&grant_type=client_credentials' ||
|
||||
options ===
|
||||
'https://oauth.vk.com/access_token?client_id=appId&client_secret=appSecret&v=5.124&grant_type=client_credentials'
|
||||
) {
|
||||
return {
|
||||
access_token: 'access_token',
|
||||
};
|
||||
}
|
||||
return Promise.resolve(responses[providerName] || { id: 'userId' });
|
||||
});
|
||||
spyOn(require('../lib/Adapters/Auth/httpsRequest'), 'request').and.callFake(() => {
|
||||
return Promise.resolve(responses[providerName] || { id: 'userId' });
|
||||
});
|
||||
const provider = require('../lib/Adapters/Auth/' + providerName);
|
||||
let params = {};
|
||||
if (providerName === 'vkontakte') {
|
||||
params = {
|
||||
appIds: 'appId',
|
||||
appSecret: 'appSecret',
|
||||
};
|
||||
await provider.validateAuthData({ id: 'userId' }, params);
|
||||
params.appVersion = '5.123';
|
||||
}
|
||||
await provider.validateAuthData({ id: 'userId' }, params);
|
||||
});
|
||||
});
|
||||
|
||||
const getMockMyOauthProvider = function () {
|
||||
return {
|
||||
authData: {
|
||||
@@ -568,46 +477,6 @@ describe('AuthenticationProviders', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('instagram auth adapter', () => {
|
||||
const instagram = require('../lib/Adapters/Auth/instagram');
|
||||
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
|
||||
|
||||
it('should use default api', async () => {
|
||||
spyOn(httpsRequest, 'get').and.callFake(() => {
|
||||
return Promise.resolve({ data: { id: 'userId' } });
|
||||
});
|
||||
await instagram.validateAuthData({ id: 'userId', access_token: 'the_token' }, {});
|
||||
expect(httpsRequest.get).toHaveBeenCalledWith(
|
||||
'https://graph.instagram.com/me?fields=id&access_token=the_token'
|
||||
);
|
||||
});
|
||||
it('response object without data child', async () => {
|
||||
spyOn(httpsRequest, 'get').and.callFake(() => {
|
||||
return Promise.resolve({ id: 'userId' });
|
||||
});
|
||||
await instagram.validateAuthData({ id: 'userId', access_token: 'the_token' }, {});
|
||||
expect(httpsRequest.get).toHaveBeenCalledWith(
|
||||
'https://graph.instagram.com/me?fields=id&access_token=the_token'
|
||||
);
|
||||
});
|
||||
it('should pass in api url', async () => {
|
||||
spyOn(httpsRequest, 'get').and.callFake(() => {
|
||||
return Promise.resolve({ data: { id: 'userId' } });
|
||||
});
|
||||
await instagram.validateAuthData(
|
||||
{
|
||||
id: 'userId',
|
||||
access_token: 'the_token',
|
||||
apiURL: 'https://new-api.instagram.com/v1/',
|
||||
},
|
||||
{}
|
||||
);
|
||||
expect(httpsRequest.get).toHaveBeenCalledWith(
|
||||
'https://new-api.instagram.com/v1/me?fields=id&access_token=the_token'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('google auth adapter', () => {
|
||||
const google = require('../lib/Adapters/Auth/google');
|
||||
const jwt = require('jsonwebtoken');
|
||||
@@ -645,7 +514,7 @@ describe('google auth adapter', () => {
|
||||
// }
|
||||
// });
|
||||
|
||||
it('(using client id as string) should verify id_token', async () => {
|
||||
it('(using client id as string) should verify id_token (google.com)', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://accounts.google.com',
|
||||
aud: 'secret',
|
||||
@@ -663,7 +532,7 @@ describe('google auth adapter', () => {
|
||||
expect(result).toEqual(fakeClaim);
|
||||
});
|
||||
|
||||
it('(using client id as string) should throw error with with invalid jwt issuer', async () => {
|
||||
it('(using client id as string) should throw error with with invalid jwt issuer (google.com)', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://not.google.com',
|
||||
sub: 'the_user_id',
|
||||
@@ -730,35 +599,6 @@ describe('google auth adapter', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('google play games service auth', () => {
|
||||
const gpgames = require('../lib/Adapters/Auth/gpgames');
|
||||
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
|
||||
|
||||
it('validateAuthData should pass validation', async () => {
|
||||
spyOn(httpsRequest, 'get').and.callFake(() => {
|
||||
return Promise.resolve({ playerId: 'userId' });
|
||||
});
|
||||
await gpgames.validateAuthData({
|
||||
id: 'userId',
|
||||
access_token: 'access_token',
|
||||
});
|
||||
});
|
||||
|
||||
it('validateAuthData should throw error', async () => {
|
||||
spyOn(httpsRequest, 'get').and.callFake(() => {
|
||||
return Promise.resolve({ playerId: 'invalid' });
|
||||
});
|
||||
try {
|
||||
await gpgames.validateAuthData({
|
||||
id: 'userId',
|
||||
access_token: 'access_token',
|
||||
});
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('Google Play Games Services - authData is invalid for this user.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('keycloak auth adapter', () => {
|
||||
const keycloak = require('../lib/Adapters/Auth/keycloak');
|
||||
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
|
||||
@@ -987,433 +827,6 @@ describe('keycloak auth adapter', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('oauth2 auth adapter', () => {
|
||||
const oauth2 = require('../lib/Adapters/Auth/oauth2');
|
||||
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
|
||||
|
||||
it('properly loads OAuth2 adapter via the "oauth2" option', () => {
|
||||
const options = {
|
||||
oauth2Authentication: {
|
||||
oauth2: true,
|
||||
},
|
||||
};
|
||||
const loadedAuthAdapter = authenticationLoader.loadAuthAdapter('oauth2Authentication', options);
|
||||
expect(loadedAuthAdapter.adapter).toEqual(oauth2);
|
||||
});
|
||||
|
||||
it('properly loads OAuth2 adapter with options', () => {
|
||||
const options = {
|
||||
oauth2Authentication: {
|
||||
oauth2: true,
|
||||
tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
|
||||
useridField: 'sub',
|
||||
appidField: 'appId',
|
||||
appIds: ['a', 'b'],
|
||||
authorizationHeader: 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=',
|
||||
debug: true,
|
||||
},
|
||||
};
|
||||
const loadedAuthAdapter = authenticationLoader.loadAuthAdapter('oauth2Authentication', options);
|
||||
const appIds = loadedAuthAdapter.appIds;
|
||||
const providerOptions = loadedAuthAdapter.providerOptions;
|
||||
expect(providerOptions.tokenIntrospectionEndpointUrl).toEqual('https://example.com/introspect');
|
||||
expect(providerOptions.useridField).toEqual('sub');
|
||||
expect(providerOptions.appidField).toEqual('appId');
|
||||
expect(appIds).toEqual(['a', 'b']);
|
||||
expect(providerOptions.authorizationHeader).toEqual('Basic dXNlcm5hbWU6cGFzc3dvcmQ=');
|
||||
expect(providerOptions.debug).toEqual(true);
|
||||
});
|
||||
|
||||
it('validateAppId should fail if OAuth2 tokenIntrospectionEndpointUrl is not configured properly', async () => {
|
||||
const options = {
|
||||
oauth2Authentication: {
|
||||
oauth2: true,
|
||||
appIds: ['a', 'b'],
|
||||
appidField: 'appId',
|
||||
},
|
||||
};
|
||||
const authData = {
|
||||
id: 'fakeid',
|
||||
access_token: 'sometoken',
|
||||
};
|
||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'oauth2Authentication',
|
||||
options
|
||||
);
|
||||
try {
|
||||
await adapter.validateAppId(appIds, authData, providerOptions);
|
||||
} catch (e) {
|
||||
expect(e.message).toBe(
|
||||
'OAuth2 token introspection endpoint URL is missing from configuration!'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('validateAppId appidField optional', async () => {
|
||||
const options = {
|
||||
oauth2Authentication: {
|
||||
oauth2: true,
|
||||
tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
|
||||
},
|
||||
};
|
||||
const authData = {
|
||||
id: 'fakeid',
|
||||
access_token: 'sometoken',
|
||||
};
|
||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'oauth2Authentication',
|
||||
options
|
||||
);
|
||||
try {
|
||||
await adapter.validateAppId(appIds, authData, providerOptions);
|
||||
} catch (e) {
|
||||
// Should not reach here
|
||||
fail(e);
|
||||
}
|
||||
});
|
||||
|
||||
it('validateAppId should fail without appIds', async () => {
|
||||
const options = {
|
||||
oauth2Authentication: {
|
||||
oauth2: true,
|
||||
tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
|
||||
appidField: 'appId',
|
||||
},
|
||||
};
|
||||
const authData = {
|
||||
id: 'fakeid',
|
||||
access_token: 'sometoken',
|
||||
};
|
||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'oauth2Authentication',
|
||||
options
|
||||
);
|
||||
try {
|
||||
await adapter.validateAppId(appIds, authData, providerOptions);
|
||||
} catch (e) {
|
||||
expect(e.message).toBe(
|
||||
'OAuth2 configuration is missing the client app IDs ("appIds" config parameter).'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('validateAppId should fail empty appIds', async () => {
|
||||
const options = {
|
||||
oauth2Authentication: {
|
||||
oauth2: true,
|
||||
tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
|
||||
appidField: 'appId',
|
||||
appIds: [],
|
||||
},
|
||||
};
|
||||
const authData = {
|
||||
id: 'fakeid',
|
||||
access_token: 'sometoken',
|
||||
};
|
||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'oauth2Authentication',
|
||||
options
|
||||
);
|
||||
try {
|
||||
await adapter.validateAppId(appIds, authData, providerOptions);
|
||||
} catch (e) {
|
||||
expect(e.message).toBe(
|
||||
'OAuth2 configuration is missing the client app IDs ("appIds" config parameter).'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('validateAppId invalid accessToken', async () => {
|
||||
const options = {
|
||||
oauth2Authentication: {
|
||||
oauth2: true,
|
||||
tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
|
||||
appidField: 'appId',
|
||||
appIds: ['a', 'b'],
|
||||
},
|
||||
};
|
||||
const authData = {
|
||||
id: 'fakeid',
|
||||
access_token: 'sometoken',
|
||||
};
|
||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'oauth2Authentication',
|
||||
options
|
||||
);
|
||||
spyOn(httpsRequest, 'request').and.callFake(() => {
|
||||
return Promise.resolve({});
|
||||
});
|
||||
try {
|
||||
await adapter.validateAppId(appIds, authData, providerOptions);
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('OAuth2 access token is invalid for this user.');
|
||||
}
|
||||
});
|
||||
|
||||
it('validateAppId invalid accessToken appId', async () => {
|
||||
const options = {
|
||||
oauth2Authentication: {
|
||||
oauth2: true,
|
||||
tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
|
||||
appidField: 'appId',
|
||||
appIds: ['a', 'b'],
|
||||
},
|
||||
};
|
||||
const authData = {
|
||||
id: 'fakeid',
|
||||
access_token: 'sometoken',
|
||||
};
|
||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'oauth2Authentication',
|
||||
options
|
||||
);
|
||||
spyOn(httpsRequest, 'request').and.callFake(() => {
|
||||
return Promise.resolve({ active: true });
|
||||
});
|
||||
try {
|
||||
await adapter.validateAppId(appIds, authData, providerOptions);
|
||||
} catch (e) {
|
||||
expect(e.message).toBe(
|
||||
"OAuth2: the access_token's appID is empty or is not in the list of permitted appIDs in the auth configuration."
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('validateAppId valid accessToken appId', async () => {
|
||||
const options = {
|
||||
oauth2Authentication: {
|
||||
oauth2: true,
|
||||
tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
|
||||
appidField: 'appId',
|
||||
appIds: ['a', 'b'],
|
||||
},
|
||||
};
|
||||
const authData = {
|
||||
id: 'fakeid',
|
||||
access_token: 'sometoken',
|
||||
};
|
||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'oauth2Authentication',
|
||||
options
|
||||
);
|
||||
spyOn(httpsRequest, 'request').and.callFake(() => {
|
||||
return Promise.resolve({
|
||||
active: true,
|
||||
appId: 'a',
|
||||
});
|
||||
});
|
||||
try {
|
||||
await adapter.validateAppId(appIds, authData, providerOptions);
|
||||
} catch (e) {
|
||||
// Should not enter here
|
||||
fail(e);
|
||||
}
|
||||
});
|
||||
|
||||
it('validateAppId valid accessToken appId array', async () => {
|
||||
const options = {
|
||||
oauth2Authentication: {
|
||||
oauth2: true,
|
||||
tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
|
||||
appidField: 'appId',
|
||||
appIds: ['a', 'b'],
|
||||
},
|
||||
};
|
||||
const authData = {
|
||||
id: 'fakeid',
|
||||
access_token: 'sometoken',
|
||||
};
|
||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'oauth2Authentication',
|
||||
options
|
||||
);
|
||||
spyOn(httpsRequest, 'request').and.callFake(() => {
|
||||
return Promise.resolve({
|
||||
active: true,
|
||||
appId: ['a'],
|
||||
});
|
||||
});
|
||||
try {
|
||||
await adapter.validateAppId(appIds, authData, providerOptions);
|
||||
} catch (e) {
|
||||
// Should not enter here
|
||||
fail(e);
|
||||
}
|
||||
});
|
||||
|
||||
it('validateAppId valid accessToken invalid appId', async () => {
|
||||
const options = {
|
||||
oauth2Authentication: {
|
||||
oauth2: true,
|
||||
tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
|
||||
appidField: 'appId',
|
||||
appIds: ['a', 'b'],
|
||||
},
|
||||
};
|
||||
const authData = {
|
||||
id: 'fakeid',
|
||||
access_token: 'sometoken',
|
||||
};
|
||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'oauth2Authentication',
|
||||
options
|
||||
);
|
||||
spyOn(httpsRequest, 'request').and.callFake(() => {
|
||||
return Promise.resolve({
|
||||
active: true,
|
||||
appId: 'unknown',
|
||||
});
|
||||
});
|
||||
try {
|
||||
await adapter.validateAppId(appIds, authData, providerOptions);
|
||||
} catch (e) {
|
||||
expect(e.message).toBe(
|
||||
"OAuth2: the access_token's appID is empty or is not in the list of permitted appIDs in the auth configuration."
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('validateAuthData should fail if OAuth2 tokenIntrospectionEndpointUrl is not configured properly', async () => {
|
||||
const options = {
|
||||
oauth2Authentication: {
|
||||
oauth2: true,
|
||||
},
|
||||
};
|
||||
const authData = {
|
||||
id: 'fakeid',
|
||||
access_token: 'sometoken',
|
||||
};
|
||||
const { adapter, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'oauth2Authentication',
|
||||
options
|
||||
);
|
||||
try {
|
||||
await adapter.validateAuthData(authData, providerOptions);
|
||||
} catch (e) {
|
||||
expect(e.message).toBe(
|
||||
'OAuth2 token introspection endpoint URL is missing from configuration!'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('validateAuthData invalid accessToken', async () => {
|
||||
const options = {
|
||||
oauth2Authentication: {
|
||||
oauth2: true,
|
||||
tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
|
||||
useridField: 'sub',
|
||||
appidField: 'appId',
|
||||
appIds: ['a', 'b'],
|
||||
authorizationHeader: 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=',
|
||||
},
|
||||
};
|
||||
const authData = {
|
||||
id: 'fakeid',
|
||||
access_token: 'sometoken',
|
||||
};
|
||||
const { adapter, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'oauth2Authentication',
|
||||
options
|
||||
);
|
||||
spyOn(httpsRequest, 'request').and.callFake(() => {
|
||||
return Promise.resolve({});
|
||||
});
|
||||
try {
|
||||
await adapter.validateAuthData(authData, providerOptions);
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('OAuth2 access token is invalid for this user.');
|
||||
}
|
||||
expect(httpsRequest.request).toHaveBeenCalledWith(
|
||||
{
|
||||
hostname: 'example.com',
|
||||
path: '/introspect',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Content-Length': 15,
|
||||
Authorization: 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=',
|
||||
},
|
||||
},
|
||||
'token=sometoken'
|
||||
);
|
||||
});
|
||||
|
||||
it('validateAuthData valid accessToken', async () => {
|
||||
const options = {
|
||||
oauth2Authentication: {
|
||||
oauth2: true,
|
||||
tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
|
||||
useridField: 'sub',
|
||||
appidField: 'appId',
|
||||
appIds: ['a', 'b'],
|
||||
},
|
||||
};
|
||||
const authData = {
|
||||
id: 'fakeid',
|
||||
access_token: 'sometoken',
|
||||
};
|
||||
const { adapter, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'oauth2Authentication',
|
||||
options
|
||||
);
|
||||
spyOn(httpsRequest, 'request').and.callFake(() => {
|
||||
return Promise.resolve({
|
||||
active: true,
|
||||
sub: 'fakeid',
|
||||
});
|
||||
});
|
||||
try {
|
||||
await adapter.validateAuthData(authData, providerOptions);
|
||||
} catch (e) {
|
||||
// Should not enter here
|
||||
fail(e);
|
||||
}
|
||||
expect(httpsRequest.request).toHaveBeenCalledWith(
|
||||
{
|
||||
hostname: 'example.com',
|
||||
path: '/introspect',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Content-Length': 15,
|
||||
},
|
||||
},
|
||||
'token=sometoken'
|
||||
);
|
||||
});
|
||||
|
||||
it('validateAuthData valid accessToken without useridField', async () => {
|
||||
const options = {
|
||||
oauth2Authentication: {
|
||||
oauth2: true,
|
||||
tokenIntrospectionEndpointUrl: 'https://example.com/introspect',
|
||||
appidField: 'appId',
|
||||
appIds: ['a', 'b'],
|
||||
},
|
||||
};
|
||||
const authData = {
|
||||
id: 'fakeid',
|
||||
access_token: 'sometoken',
|
||||
};
|
||||
const { adapter, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'oauth2Authentication',
|
||||
options
|
||||
);
|
||||
spyOn(httpsRequest, 'request').and.callFake(() => {
|
||||
return Promise.resolve({
|
||||
active: true,
|
||||
sub: 'fakeid',
|
||||
});
|
||||
});
|
||||
try {
|
||||
await adapter.validateAuthData(authData, providerOptions);
|
||||
} catch (e) {
|
||||
// Should not enter here
|
||||
fail(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('apple signin auth adapter', () => {
|
||||
const apple = require('../lib/Adapters/Auth/apple');
|
||||
const jwt = require('jsonwebtoken');
|
||||
@@ -1466,7 +879,7 @@ describe('apple signin auth adapter', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should use algorithm from key header to verify id_token', async () => {
|
||||
it('should use algorithm from key header to verify id_token (apple.com)', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://appleid.apple.com',
|
||||
aud: 'secret',
|
||||
@@ -1516,7 +929,7 @@ describe('apple signin auth adapter', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('(using client id as string) should verify id_token', async () => {
|
||||
it('(using client id as string) should verify id_token (apple.com)', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://appleid.apple.com',
|
||||
aud: 'secret',
|
||||
@@ -1536,7 +949,7 @@ describe('apple signin auth adapter', () => {
|
||||
expect(result).toEqual(fakeClaim);
|
||||
});
|
||||
|
||||
it('(using client id as array) should verify id_token', async () => {
|
||||
it('(using client id as array) should verify id_token (apple.com)', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://appleid.apple.com',
|
||||
aud: 'secret',
|
||||
@@ -1556,7 +969,7 @@ describe('apple signin auth adapter', () => {
|
||||
expect(result).toEqual(fakeClaim);
|
||||
});
|
||||
|
||||
it('(using client id as array with multiple items) should verify id_token', async () => {
|
||||
it('(using client id as array with multiple items) should verify id_token (apple.com)', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://appleid.apple.com',
|
||||
aud: 'secret',
|
||||
@@ -1576,7 +989,7 @@ describe('apple signin auth adapter', () => {
|
||||
expect(result).toEqual(fakeClaim);
|
||||
});
|
||||
|
||||
it('(using client id as string) should throw error with with invalid jwt issuer', async () => {
|
||||
it('(using client id as string) should throw error with with invalid jwt issuer (apple.com)', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://not.apple.com',
|
||||
sub: 'the_user_id',
|
||||
@@ -1629,7 +1042,7 @@ describe('apple signin auth adapter', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('(using client id as string) should throw error with with invalid jwt issuer with token', async () => {
|
||||
it('(using client id as string) should throw error with with invalid jwt issuer with token (apple.com)', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://not.apple.com',
|
||||
sub: 'the_user_id',
|
||||
@@ -1698,7 +1111,7 @@ describe('apple signin auth adapter', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw error with with invalid user id', async () => {
|
||||
it('should throw error with with invalid user id (apple.com)', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://appleid.apple.com',
|
||||
aud: 'invalid_client_id',
|
||||
@@ -1722,206 +1135,17 @@ describe('apple signin auth adapter', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Apple Game Center Auth adapter', () => {
|
||||
const gcenter = require('../lib/Adapters/Auth/gcenter');
|
||||
const fs = require('fs');
|
||||
const testCert = fs.readFileSync(__dirname + '/support/cert/game_center.pem');
|
||||
const testCert2 = fs.readFileSync(__dirname + '/support/cert/game_center.pem');
|
||||
|
||||
it('can load adapter', async () => {
|
||||
const options = {
|
||||
gcenter: {
|
||||
rootCertificateUrl:
|
||||
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
|
||||
},
|
||||
};
|
||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'gcenter',
|
||||
options
|
||||
);
|
||||
await adapter.validateAppId(
|
||||
appIds,
|
||||
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
|
||||
providerOptions
|
||||
);
|
||||
});
|
||||
|
||||
it('validateAuthData should validate', async () => {
|
||||
const options = {
|
||||
gcenter: {
|
||||
rootCertificateUrl:
|
||||
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
|
||||
},
|
||||
};
|
||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'gcenter',
|
||||
options
|
||||
);
|
||||
await adapter.validateAppId(
|
||||
appIds,
|
||||
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
|
||||
providerOptions
|
||||
);
|
||||
// real token is used
|
||||
const authData = {
|
||||
id: 'G:1965586982',
|
||||
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||
timestamp: 1565257031287,
|
||||
signature:
|
||||
'uqLBTr9Uex8zCpc1UQ1MIDMitb+HUat2Mah4Kw6AVLSGe0gGNJXlih2i5X+0ZwVY0S9zY2NHWi2gFjmhjt/4kxWGMkupqXX5H/qhE2m7hzox6lZJpH98ZEUbouWRfZX2ZhUlCkAX09oRNi7fI7mWL1/o88MaI/y6k6tLr14JTzmlxgdyhw+QRLxRPA6NuvUlRSJpyJ4aGtNH5/wHdKQWL8nUnFYiYmaY8R7IjzNxPfy8UJTUWmeZvMSgND4u8EjADPsz7ZtZyWAPi8kYcAb6M8k0jwLD3vrYCB8XXyO2RQb/FY2TM4zJuI7PzLlvvgOJXbbfVtHx7Evnm5NYoyzgzw==',
|
||||
salt: 'DzqqrQ==',
|
||||
bundleId: 'cloud.xtralife.gamecenterauth',
|
||||
};
|
||||
gcenter.cache['https://static.gc.apple.com/public-key/gc-prod-4.cer'] = testCert;
|
||||
await gcenter.validateAuthData(authData);
|
||||
});
|
||||
|
||||
it('validateAuthData invalid signature id', async () => {
|
||||
gcenter.cache['https://static.gc.apple.com/public-key/gc-prod-4.cer'] = testCert;
|
||||
gcenter.cache['https://static.gc.apple.com/public-key/gc-prod-6.cer'] = testCert2;
|
||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'gcenter',
|
||||
{}
|
||||
);
|
||||
await adapter.validateAppId(
|
||||
appIds,
|
||||
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
|
||||
providerOptions
|
||||
);
|
||||
const authData = {
|
||||
id: 'G:1965586982',
|
||||
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-6.cer',
|
||||
timestamp: 1565257031287,
|
||||
signature: '1234',
|
||||
salt: 'DzqqrQ==',
|
||||
bundleId: 'com.example.com',
|
||||
};
|
||||
await expectAsync(gcenter.validateAuthData(authData)).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Apple Game Center - invalid signature')
|
||||
);
|
||||
});
|
||||
|
||||
it('validateAuthData invalid public key http url', async () => {
|
||||
const options = {
|
||||
gcenter: {
|
||||
rootCertificateUrl:
|
||||
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
|
||||
},
|
||||
};
|
||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'gcenter',
|
||||
options
|
||||
);
|
||||
await adapter.validateAppId(
|
||||
appIds,
|
||||
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
|
||||
providerOptions
|
||||
);
|
||||
const publicKeyUrls = [
|
||||
'example.com',
|
||||
'http://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||
'https://developer.apple.com/assets/elements/badges/download-on-the-app-store.svg',
|
||||
'https://example.com/ \\.apple.com/public_key.cer',
|
||||
'https://example.com/ &.apple.com/public_key.cer',
|
||||
];
|
||||
await Promise.all(
|
||||
publicKeyUrls.map(publicKeyUrl =>
|
||||
expectAsync(
|
||||
gcenter.validateAuthData({
|
||||
id: 'G:1965586982',
|
||||
timestamp: 1565257031287,
|
||||
publicKeyUrl,
|
||||
signature: '1234',
|
||||
salt: 'DzqqrQ==',
|
||||
bundleId: 'com.example.com',
|
||||
})
|
||||
).toBeRejectedWith(
|
||||
new Parse.Error(
|
||||
Parse.Error.SCRIPT_FAILED,
|
||||
`Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('should not validate Symantec Cert', async () => {
|
||||
const options = {
|
||||
gcenter: {
|
||||
rootCertificateUrl:
|
||||
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
|
||||
},
|
||||
};
|
||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'gcenter',
|
||||
options
|
||||
);
|
||||
await adapter.validateAppId(
|
||||
appIds,
|
||||
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
|
||||
providerOptions
|
||||
);
|
||||
expect(() =>
|
||||
gcenter.verifyPublicKeyIssuer(
|
||||
testCert,
|
||||
'https://static.gc.apple.com/public-key/gc-prod-4.cer'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('adapter should load default cert', async () => {
|
||||
const options = {
|
||||
gcenter: {},
|
||||
};
|
||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'gcenter',
|
||||
options
|
||||
);
|
||||
await adapter.validateAppId(
|
||||
appIds,
|
||||
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
|
||||
providerOptions
|
||||
);
|
||||
const previous = new Date();
|
||||
await adapter.validateAppId(
|
||||
appIds,
|
||||
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
|
||||
providerOptions
|
||||
);
|
||||
|
||||
const duration = new Date().getTime() - previous.getTime();
|
||||
expect(duration <= 1).toBe(true);
|
||||
});
|
||||
|
||||
it('adapter should throw', async () => {
|
||||
const options = {
|
||||
gcenter: {
|
||||
rootCertificateUrl: 'https://example.com',
|
||||
},
|
||||
};
|
||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'gcenter',
|
||||
options
|
||||
);
|
||||
await expectAsync(
|
||||
adapter.validateAppId(
|
||||
appIds,
|
||||
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
|
||||
providerOptions
|
||||
)
|
||||
).toBeRejectedWith(
|
||||
new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Apple Game Center auth adapter parameter `rootCertificateURL` is invalid.'
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('phant auth adapter', () => {
|
||||
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
|
||||
|
||||
it('validateAuthData should throw for invalid auth', async () => {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
phantauth: {
|
||||
enableInsecureAuth: true,
|
||||
}
|
||||
}
|
||||
})
|
||||
const authData = {
|
||||
id: 'fakeid',
|
||||
access_token: 'sometoken',
|
||||
@@ -1938,34 +1162,6 @@ describe('phant auth adapter', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('microsoft graph auth adapter', () => {
|
||||
const microsoft = require('../lib/Adapters/Auth/microsoft');
|
||||
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
|
||||
|
||||
it('should use access_token for validation is passed and responds with id and mail', async () => {
|
||||
spyOn(httpsRequest, 'get').and.callFake(() => {
|
||||
return Promise.resolve({ id: 'userId', mail: 'userMail' });
|
||||
});
|
||||
await microsoft.validateAuthData({
|
||||
id: 'userId',
|
||||
access_token: 'the_token',
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to validate Microsoft Graph auth with bad token', done => {
|
||||
const authData = {
|
||||
id: 'fake-id',
|
||||
mail: 'fake@mail.com',
|
||||
access_token: 'very.long.bad.token',
|
||||
};
|
||||
microsoft.validateAuthData(authData).then(done.fail, err => {
|
||||
expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
|
||||
expect(err.message).toBe('Microsoft Graph auth is invalid for this user.');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('facebook limited auth adapter', () => {
|
||||
const facebook = require('../lib/Adapters/Auth/facebook');
|
||||
const jwt = require('jsonwebtoken');
|
||||
@@ -2022,7 +1218,7 @@ describe('facebook limited auth adapter', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should use algorithm from key header to verify id_token', async () => {
|
||||
it_id('7bfa55ab-8fd7-4526-992e-6de3df16bf9c')(it)('should use algorithm from key header to verify id_token (facebook.com)', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://www.facebook.com',
|
||||
aud: 'secret',
|
||||
@@ -2072,7 +1268,7 @@ describe('facebook limited auth adapter', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('(using client id as string) should verify id_token', async () => {
|
||||
it_id('4bcb1a1a-11f8-4e12-a3f6-73f7e25e355a')(it)('using client id as string) should verify id_token (facebook.com)', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://www.facebook.com',
|
||||
aud: 'secret',
|
||||
@@ -2092,7 +1288,7 @@ describe('facebook limited auth adapter', () => {
|
||||
expect(result).toEqual(fakeClaim);
|
||||
});
|
||||
|
||||
it('(using client id as array) should verify id_token', async () => {
|
||||
it_id('c521a272-2ac2-4d8b-b5ed-ea250336d8b1')(it)('(using client id as array) should verify id_token (facebook.com)', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://www.facebook.com',
|
||||
aud: 'secret',
|
||||
@@ -2112,7 +1308,7 @@ describe('facebook limited auth adapter', () => {
|
||||
expect(result).toEqual(fakeClaim);
|
||||
});
|
||||
|
||||
it('(using client id as array with multiple items) should verify id_token', async () => {
|
||||
it_id('e3f16404-18e9-4a87-a555-4710cfbdac67')(it)('(using client id as array with multiple items) should verify id_token (facebook.com)', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://www.facebook.com',
|
||||
aud: 'secret',
|
||||
@@ -2132,7 +1328,7 @@ describe('facebook limited auth adapter', () => {
|
||||
expect(result).toEqual(fakeClaim);
|
||||
});
|
||||
|
||||
it('(using client id as string) should throw error with with invalid jwt issuer', async () => {
|
||||
it_id('549c33a1-3a6b-4732-8cf6-8f010ad4569c')(it)('(using client id as string) should throw error with with invalid jwt issuer (facebook.com)', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://not.facebook.com',
|
||||
sub: 'the_user_id',
|
||||
@@ -2263,7 +1459,7 @@ describe('facebook limited auth adapter', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw error with with invalid user id', async () => {
|
||||
it_id('c194d902-e697-46c9-a303-82c2d914473c')(it)('should throw error with with invalid user id (facebook.com)', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://www.facebook.com',
|
||||
aud: 'invalid_client_id',
|
||||
|
||||
@@ -355,16 +355,16 @@ describe('Auth Adapter features', () => {
|
||||
const authData = user.get('authData').modernAdapter3;
|
||||
expect(authData).toEqual({ foo: 'bar' });
|
||||
for (const call of afterSpy.calls.all()) {
|
||||
const args = call.args[0];
|
||||
const args = call.args[2];
|
||||
if (args.user) {
|
||||
user._objCount = args.user._objCount;
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(afterSpy).toHaveBeenCalledWith(
|
||||
{ ip: '127.0.0.1', user, master: false },
|
||||
{ id: 'modernAdapter3Data' },
|
||||
undefined
|
||||
undefined,
|
||||
{ ip: '127.0.0.1', user, master: false },
|
||||
);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
+11
-5
@@ -1,5 +1,5 @@
|
||||
'use strict';
|
||||
const commander = require('../lib/cli/utils/commander').default;
|
||||
let commander;
|
||||
const definitions = require('../lib/cli/definitions/parse-server').default;
|
||||
const liveQueryDefinitions = require('../lib/cli/definitions/parse-live-query-server').default;
|
||||
const path = require('path');
|
||||
@@ -28,6 +28,12 @@ const testDefinitions = {
|
||||
};
|
||||
|
||||
describe('commander additions', () => {
|
||||
beforeEach(() => {
|
||||
const command = require('../lib/cli/utils/commander').default;
|
||||
commander = new command.constructor();
|
||||
commander.storeOptionsAsProperties();
|
||||
commander.allowExcessArguments();
|
||||
});
|
||||
afterEach(done => {
|
||||
commander.options = [];
|
||||
delete commander.arg0;
|
||||
@@ -248,7 +254,7 @@ describe('execution', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should start Parse Server', done => {
|
||||
it_id('a0ab74b4-f805-4e03-b31d-b5cd59e64495')(it)('should start Parse Server', done => {
|
||||
const env = { ...process.env };
|
||||
env.NODE_OPTIONS = '--dns-result-order=ipv4first --trace-deprecation';
|
||||
childProcess = spawn(
|
||||
@@ -261,7 +267,7 @@ describe('execution', () => {
|
||||
handleError(childProcess, done);
|
||||
});
|
||||
|
||||
it('should start Parse Server with GraphQL', async done => {
|
||||
it_id('d7165081-b133-4cba-901b-19128ce41301')(it)('should start Parse Server with GraphQL', async done => {
|
||||
const env = { ...process.env };
|
||||
env.NODE_OPTIONS = '--dns-result-order=ipv4first --trace-deprecation';
|
||||
childProcess = spawn(
|
||||
@@ -287,7 +293,7 @@ describe('execution', () => {
|
||||
handleError(childProcess, done);
|
||||
});
|
||||
|
||||
it('should start Parse Server with GraphQL and Playground', async done => {
|
||||
it_id('2769cdb4-ce8a-484d-8a91-635b5894ba7e')(it)('should start Parse Server with GraphQL and Playground', async done => {
|
||||
const env = { ...process.env };
|
||||
env.NODE_OPTIONS = '--dns-result-order=ipv4first --trace-deprecation';
|
||||
childProcess = spawn(
|
||||
@@ -315,7 +321,7 @@ describe('execution', () => {
|
||||
handleError(childProcess, done);
|
||||
});
|
||||
|
||||
it('can start Parse Server with auth via CLI', done => {
|
||||
it_id('23caddd7-bfea-4869-8bd4-0f2cd283c8bd')(it)('can start Parse Server with auth via CLI', done => {
|
||||
const env = { ...process.env };
|
||||
env.NODE_OPTIONS = '--dns-result-order=ipv4first --trace-deprecation';
|
||||
childProcess = spawn(
|
||||
|
||||
@@ -6,6 +6,9 @@ const validatorFail = () => {
|
||||
const validatorSuccess = () => {
|
||||
return true;
|
||||
};
|
||||
function testConfig() {
|
||||
return Parse.Config.save({ internal: 'i', string: 's', number: 12 }, { internal: true });
|
||||
}
|
||||
|
||||
describe('cloud validator', () => {
|
||||
it('complete validator', async done => {
|
||||
@@ -731,6 +734,38 @@ describe('cloud validator', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it_id('893eec0c-41bd-4adf-8f0a-306087ad8d61')(it)('basic beforeSave Parse.Config skipWithMasterKey', async () => {
|
||||
Parse.Cloud.beforeSave(
|
||||
Parse.Config,
|
||||
() => {
|
||||
throw 'beforeSaveFile should have resolved using master key.';
|
||||
},
|
||||
{
|
||||
skipWithMasterKey: true,
|
||||
}
|
||||
);
|
||||
const config = await testConfig();
|
||||
expect(config.get('internal')).toBe('i');
|
||||
expect(config.get('string')).toBe('s');
|
||||
expect(config.get('number')).toBe(12);
|
||||
});
|
||||
|
||||
it_id('91e739a4-6a38-405c-8f83-f36d48220734')(it)('basic afterSave Parse.Config skipWithMasterKey', async () => {
|
||||
Parse.Cloud.afterSave(
|
||||
Parse.Config,
|
||||
() => {
|
||||
throw 'beforeSaveFile should have resolved using master key.';
|
||||
},
|
||||
{
|
||||
skipWithMasterKey: true,
|
||||
}
|
||||
);
|
||||
const config = await testConfig();
|
||||
expect(config.get('internal')).toBe('i');
|
||||
expect(config.get('string')).toBe('s');
|
||||
expect(config.get('number')).toBe(12);
|
||||
});
|
||||
|
||||
it('beforeSave validateMasterKey and skipWithMasterKey fail', async function (done) {
|
||||
Parse.Cloud.beforeSave(
|
||||
'BeforeSave',
|
||||
@@ -1441,7 +1476,7 @@ describe('cloud validator', () => {
|
||||
});
|
||||
|
||||
it('validate afterSaveFile fail', async done => {
|
||||
Parse.Cloud.beforeSave(Parse.File, () => {}, validatorFail);
|
||||
Parse.Cloud.afterSave(Parse.File, () => {}, validatorFail);
|
||||
try {
|
||||
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
|
||||
await file.save({ useMasterKey: true });
|
||||
@@ -1496,6 +1531,42 @@ describe('cloud validator', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it_id('32ca1a99-7f2b-429d-a7cf-62b6661d0af6')(it)('validate beforeSave Parse.Config', async () => {
|
||||
Parse.Cloud.beforeSave(Parse.Config, () => {}, validatorSuccess);
|
||||
const config = await testConfig();
|
||||
expect(config.get('internal')).toBe('i');
|
||||
expect(config.get('string')).toBe('s');
|
||||
expect(config.get('number')).toBe(12);
|
||||
});
|
||||
|
||||
it_id('c84d11e7-d09c-4843-ad98-f671511bf612')(it)('validate beforeSave Parse.Config fail', async () => {
|
||||
Parse.Cloud.beforeSave(Parse.Config, () => {}, validatorFail);
|
||||
try {
|
||||
await testConfig();
|
||||
fail('cloud function should have failed.');
|
||||
} catch (e) {
|
||||
expect(e.code).toBe(Parse.Error.VALIDATION_ERROR);
|
||||
}
|
||||
});
|
||||
|
||||
it_id('b18b9a6a-0e35-4b60-9771-30f53501df3c')(it)('validate afterSave Parse.Config', async () => {
|
||||
Parse.Cloud.afterSave(Parse.Config, () => {}, validatorSuccess);
|
||||
const config = await testConfig();
|
||||
expect(config.get('internal')).toBe('i');
|
||||
expect(config.get('string')).toBe('s');
|
||||
expect(config.get('number')).toBe(12);
|
||||
});
|
||||
|
||||
it_id('ef761222-1758-4614-b984-da84d73fc10c')(it)('validate afterSave Parse.Config fail', async () => {
|
||||
Parse.Cloud.afterSave(Parse.Config, () => {}, validatorFail);
|
||||
try {
|
||||
await testConfig();
|
||||
fail('cloud function should have failed.');
|
||||
} catch (e) {
|
||||
expect(e.code).toBe(Parse.Error.VALIDATION_ERROR);
|
||||
}
|
||||
});
|
||||
|
||||
it('Should have validator', async done => {
|
||||
Parse.Cloud.define(
|
||||
'myFunction',
|
||||
|
||||
+293
-9
@@ -2317,7 +2317,7 @@ describe('beforeFind hooks', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty where', done => {
|
||||
it_id('6ef0d226-af30-4dfd-8306-972a1b4becd3')(it)('should handle empty where', done => {
|
||||
Parse.Cloud.beforeFind('MyObject', req => {
|
||||
const otherQuery = new Parse.Query('MyObject');
|
||||
otherQuery.equalTo('some', true);
|
||||
@@ -2919,7 +2919,7 @@ describe('afterFind hooks', () => {
|
||||
}).toThrow('Only the _Session class is allowed for the afterLogout trigger.');
|
||||
});
|
||||
|
||||
it('should skip afterFind hooks for aggregate', done => {
|
||||
it_id('c16159b5-e8ee-42d5-8fe3-e2f7c006881d')(it)('should skip afterFind hooks for aggregate', done => {
|
||||
const hook = {
|
||||
method: function () {
|
||||
return Promise.reject();
|
||||
@@ -2946,7 +2946,7 @@ describe('afterFind hooks', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip afterFind hooks for distinct', done => {
|
||||
it_id('ca55c90d-36db-422c-9060-a30583ce5224')(it)('should skip afterFind hooks for distinct', done => {
|
||||
const hook = {
|
||||
method: function () {
|
||||
return Promise.reject();
|
||||
@@ -3033,7 +3033,7 @@ describe('afterFind hooks', () => {
|
||||
expect(calledAfter).toBe(false);
|
||||
});
|
||||
|
||||
it('should expose context in beforeSave/afterSave via header', async () => {
|
||||
it_id('55ef1741-cf72-4a7c-a029-00cb75f53233')(it)('should expose context in beforeSave/afterSave via header', async () => {
|
||||
let calledBefore = false;
|
||||
let calledAfter = false;
|
||||
Parse.Cloud.beforeSave('TestObject', req => {
|
||||
@@ -3347,7 +3347,7 @@ describe('beforeLogin hook', () => {
|
||||
expect(response).toEqual(error);
|
||||
});
|
||||
|
||||
it('should have expected data in request', async done => {
|
||||
it_id('5656d6d7-65ef-43d1-8ca6-6942ae3614d5')(it)('should have expected data in request in beforeLogin', async done => {
|
||||
Parse.Cloud.beforeLogin(req => {
|
||||
expect(req.object).toBeDefined();
|
||||
expect(req.user).toBeUndefined();
|
||||
@@ -3464,7 +3464,7 @@ describe('afterLogin hook', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should have expected data in request', async done => {
|
||||
it_id('e86155c4-62e1-4c6e-ab4a-9ac6c87c60f2')(it)('should have expected data in request in afterLogin', async done => {
|
||||
Parse.Cloud.afterLogin(req => {
|
||||
expect(req.object).toBeDefined();
|
||||
expect(req.user).toBeDefined();
|
||||
@@ -3494,9 +3494,17 @@ describe('afterLogin hook', () => {
|
||||
expect(req.object._context).not.toBeDefined();
|
||||
expect(req.object.context).not.toBeDefined();
|
||||
});
|
||||
const obj = new TestObject();
|
||||
obj.set('_context', { hello: 'world' });
|
||||
await obj.save(null, { context: { a: 'a' } });
|
||||
await request({
|
||||
url: 'http://localhost:8378/1/classes/TestObject',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Cloud-Context': '{"a":"a"}',
|
||||
},
|
||||
body: JSON.stringify({ _context: { hello: 'world' } }),
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should have access to context when saving a new object', async () => {
|
||||
@@ -3921,6 +3929,282 @@ describe('saveFile hooks', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Parse.File hooks', () => {
|
||||
it('find hooks should run', async () => {
|
||||
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
|
||||
await file.save({ useMasterKey: true });
|
||||
const user = await Parse.User.signUp('username', 'password');
|
||||
const hooks = {
|
||||
beforeFind(req) {
|
||||
expect(req).toBeDefined();
|
||||
expect(req.file).toBeDefined();
|
||||
expect(req.triggerName).toBe('beforeFind');
|
||||
expect(req.master).toBeFalse();
|
||||
expect(req.log).toBeDefined();
|
||||
},
|
||||
afterFind(req) {
|
||||
expect(req).toBeDefined();
|
||||
expect(req.file).toBeDefined();
|
||||
expect(req.triggerName).toBe('afterFind');
|
||||
expect(req.master).toBeFalse();
|
||||
expect(req.log).toBeDefined();
|
||||
expect(req.forceDownload).toBeFalse();
|
||||
},
|
||||
};
|
||||
for (const hook in hooks) {
|
||||
spyOn(hooks, hook).and.callThrough();
|
||||
Parse.Cloud[hook](Parse.File, hooks[hook]);
|
||||
}
|
||||
await request({
|
||||
url: file.url(),
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Session-Token': user.getSessionToken(),
|
||||
},
|
||||
});
|
||||
for (const hook in hooks) {
|
||||
expect(hooks[hook]).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
|
||||
it('beforeFind can throw', async () => {
|
||||
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
|
||||
await file.save({ useMasterKey: true });
|
||||
const user = await Parse.User.signUp('username', 'password');
|
||||
const hooks = {
|
||||
beforeFind() {
|
||||
throw 'unauthorized';
|
||||
},
|
||||
afterFind() {},
|
||||
};
|
||||
for (const hook in hooks) {
|
||||
spyOn(hooks, hook).and.callThrough();
|
||||
Parse.Cloud[hook](Parse.File, hooks[hook]);
|
||||
}
|
||||
await expectAsync(
|
||||
request({
|
||||
url: file.url(),
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Session-Token': user.getSessionToken(),
|
||||
},
|
||||
}).catch(e => {
|
||||
throw new Parse.Error(e.data.code, e.data.error);
|
||||
})
|
||||
).toBeRejectedWith(new Parse.Error(Parse.Error.SCRIPT_FAILED, 'unauthorized'));
|
||||
|
||||
expect(hooks.beforeFind).toHaveBeenCalled();
|
||||
expect(hooks.afterFind).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('afterFind can throw', async () => {
|
||||
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
|
||||
await file.save({ useMasterKey: true });
|
||||
const user = await Parse.User.signUp('username', 'password');
|
||||
const hooks = {
|
||||
beforeFind() {},
|
||||
afterFind() {
|
||||
throw 'unauthorized';
|
||||
},
|
||||
};
|
||||
for (const hook in hooks) {
|
||||
spyOn(hooks, hook).and.callThrough();
|
||||
Parse.Cloud[hook](Parse.File, hooks[hook]);
|
||||
}
|
||||
await expectAsync(
|
||||
request({
|
||||
url: file.url(),
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Session-Token': user.getSessionToken(),
|
||||
},
|
||||
}).catch(e => {
|
||||
throw new Parse.Error(e.data.code, e.data.error);
|
||||
})
|
||||
).toBeRejectedWith(new Parse.Error(Parse.Error.SCRIPT_FAILED, 'unauthorized'));
|
||||
for (const hook in hooks) {
|
||||
expect(hooks[hook]).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
|
||||
it('can force download', async () => {
|
||||
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
|
||||
await file.save({ useMasterKey: true });
|
||||
const user = await Parse.User.signUp('username', 'password');
|
||||
Parse.Cloud.afterFind(Parse.File, req => {
|
||||
req.forceDownload = true;
|
||||
});
|
||||
const response = await request({
|
||||
url: file.url(),
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Session-Token': user.getSessionToken(),
|
||||
},
|
||||
});
|
||||
expect(response.headers['content-disposition']).toBe(`attachment;filename=${file._name}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Cloud Config hooks', () => {
|
||||
function testConfig() {
|
||||
return Parse.Config.save({ internal: 'i', string: 's', number: 12 }, { internal: true });
|
||||
}
|
||||
|
||||
it_id('997fe20a-96f7-454a-a5b0-c155b8d02f05')(it)('beforeSave(Parse.Config) can run hook with new config', async () => {
|
||||
let count = 0;
|
||||
Parse.Cloud.beforeSave(Parse.Config, (req) => {
|
||||
expect(req.object).toBeDefined();
|
||||
expect(req.original).toBeUndefined();
|
||||
expect(req.user).toBeUndefined();
|
||||
expect(req.headers).toBeDefined();
|
||||
expect(req.ip).toBeDefined();
|
||||
expect(req.installationId).toBeDefined();
|
||||
expect(req.context).toBeDefined();
|
||||
const config = req.object;
|
||||
expect(config.get('internal')).toBe('i');
|
||||
expect(config.get('string')).toBe('s');
|
||||
expect(config.get('number')).toBe(12);
|
||||
count += 1;
|
||||
});
|
||||
await testConfig();
|
||||
const config = await Parse.Config.get({ useMasterKey: true });
|
||||
expect(config.get('internal')).toBe('i');
|
||||
expect(config.get('string')).toBe('s');
|
||||
expect(config.get('number')).toBe(12);
|
||||
expect(count).toBe(1);
|
||||
});
|
||||
|
||||
it_id('06a9b66c-ffb4-43d1-a025-f7d2192500e7')(it)('beforeSave(Parse.Config) can run hook with existing config', async () => {
|
||||
let count = 0;
|
||||
Parse.Cloud.beforeSave(Parse.Config, (req) => {
|
||||
if (count === 0) {
|
||||
expect(req.object.get('number')).toBe(12);
|
||||
expect(req.original).toBeUndefined();
|
||||
}
|
||||
if (count === 1) {
|
||||
expect(req.object.get('number')).toBe(13);
|
||||
expect(req.original.get('number')).toBe(12);
|
||||
}
|
||||
count += 1;
|
||||
});
|
||||
await testConfig();
|
||||
await Parse.Config.save({ number: 13 });
|
||||
expect(count).toBe(2);
|
||||
});
|
||||
|
||||
it_id('ca76de8e-671b-4c2d-9535-bd28a855fa1a')(it)('beforeSave(Parse.Config) should not change config if nothing is returned', async () => {
|
||||
let count = 0;
|
||||
Parse.Cloud.beforeSave(Parse.Config, () => {
|
||||
count += 1;
|
||||
return;
|
||||
});
|
||||
await testConfig();
|
||||
const config = await Parse.Config.get({ useMasterKey: true });
|
||||
expect(config.get('internal')).toBe('i');
|
||||
expect(config.get('string')).toBe('s');
|
||||
expect(config.get('number')).toBe(12);
|
||||
expect(count).toBe(1);
|
||||
});
|
||||
|
||||
it('beforeSave(Parse.Config) throw custom error', async () => {
|
||||
Parse.Cloud.beforeSave(Parse.Config, () => {
|
||||
throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'It should fail');
|
||||
});
|
||||
try {
|
||||
await testConfig();
|
||||
fail('error should have thrown');
|
||||
} catch (e) {
|
||||
expect(e.code).toBe(Parse.Error.SCRIPT_FAILED);
|
||||
expect(e.message).toBe('It should fail');
|
||||
}
|
||||
});
|
||||
|
||||
it('beforeSave(Parse.Config) throw string error', async () => {
|
||||
Parse.Cloud.beforeSave(Parse.Config, () => {
|
||||
throw 'before save failed';
|
||||
});
|
||||
try {
|
||||
await testConfig();
|
||||
fail('error should have thrown');
|
||||
} catch (e) {
|
||||
expect(e.code).toBe(Parse.Error.SCRIPT_FAILED);
|
||||
expect(e.message).toBe('before save failed');
|
||||
}
|
||||
});
|
||||
|
||||
it('beforeSave(Parse.Config) throw empty error', async () => {
|
||||
Parse.Cloud.beforeSave(Parse.Config, () => {
|
||||
throw null;
|
||||
});
|
||||
try {
|
||||
await testConfig();
|
||||
fail('error should have thrown');
|
||||
} catch (e) {
|
||||
expect(e.code).toBe(Parse.Error.SCRIPT_FAILED);
|
||||
expect(e.message).toBe('Script failed. Unknown error.');
|
||||
}
|
||||
});
|
||||
|
||||
it_id('3e7a75c0-6c2e-4c7e-b042-6eb5f23acf94')(it)('afterSave(Parse.Config) can run hook with new config', async () => {
|
||||
let count = 0;
|
||||
Parse.Cloud.afterSave(Parse.Config, (req) => {
|
||||
expect(req.object).toBeDefined();
|
||||
expect(req.original).toBeUndefined();
|
||||
expect(req.user).toBeUndefined();
|
||||
expect(req.headers).toBeDefined();
|
||||
expect(req.ip).toBeDefined();
|
||||
expect(req.installationId).toBeDefined();
|
||||
expect(req.context).toBeDefined();
|
||||
const config = req.object;
|
||||
expect(config.get('internal')).toBe('i');
|
||||
expect(config.get('string')).toBe('s');
|
||||
expect(config.get('number')).toBe(12);
|
||||
count += 1;
|
||||
});
|
||||
await testConfig();
|
||||
const config = await Parse.Config.get({ useMasterKey: true });
|
||||
expect(config.get('internal')).toBe('i');
|
||||
expect(config.get('string')).toBe('s');
|
||||
expect(config.get('number')).toBe(12);
|
||||
expect(count).toBe(1);
|
||||
});
|
||||
|
||||
it_id('5cffb28a-2924-4857-84bb-f5778d80372a')(it)('afterSave(Parse.Config) can run hook with existing config', async () => {
|
||||
let count = 0;
|
||||
Parse.Cloud.afterSave(Parse.Config, (req) => {
|
||||
if (count === 0) {
|
||||
expect(req.object.get('number')).toBe(12);
|
||||
expect(req.original).toBeUndefined();
|
||||
}
|
||||
if (count === 1) {
|
||||
expect(req.object.get('number')).toBe(13);
|
||||
expect(req.original.get('number')).toBe(12);
|
||||
}
|
||||
count += 1;
|
||||
});
|
||||
await testConfig();
|
||||
await Parse.Config.save({ number: 13 });
|
||||
expect(count).toBe(2);
|
||||
});
|
||||
|
||||
it_id('49883992-ce91-4797-85f9-7cce1f819407')(it)('afterSave(Parse.Config) should throw error', async () => {
|
||||
Parse.Cloud.afterSave(Parse.Config, () => {
|
||||
throw new Parse.Error(400, 'It should fail');
|
||||
});
|
||||
try {
|
||||
await testConfig();
|
||||
fail('error should have thrown');
|
||||
} catch (e) {
|
||||
expect(e.code).toBe(400);
|
||||
expect(e.message).toBe('It should fail');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendEmail', () => {
|
||||
it('can send email via Parse.Cloud', async done => {
|
||||
const emailAdapter = {
|
||||
|
||||
@@ -37,7 +37,7 @@ describe('Cloud Code Logger', () => {
|
||||
// Note that helpers takes care of logout.
|
||||
// see helpers.js:afterEach
|
||||
|
||||
it('should expose log to functions', () => {
|
||||
it_id('02d53b97-3ec7-46fb-abb6-176fd6e85590')(it)('should expose log to functions', () => {
|
||||
const spy = spyOn(Config.get('test').loggerController, 'log').and.callThrough();
|
||||
Parse.Cloud.define('loggerTest', req => {
|
||||
req.log.info('logTest', 'info log', { info: 'some log' });
|
||||
@@ -67,7 +67,7 @@ describe('Cloud Code Logger', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('trigger should obfuscate password', done => {
|
||||
it_id('768412f5-d32f-4134-89a6-08949781a6c0')(it)('trigger should obfuscate password', done => {
|
||||
Parse.Cloud.beforeSave(Parse.User, req => {
|
||||
return req.object;
|
||||
});
|
||||
@@ -82,7 +82,7 @@ describe('Cloud Code Logger', () => {
|
||||
.then(null, e => done.fail(e));
|
||||
});
|
||||
|
||||
it('should expose log to trigger', done => {
|
||||
it_id('3c394047-272e-4728-9d02-9eaa660d2ed2')(it)('should expose log to trigger', done => {
|
||||
Parse.Cloud.beforeSave('MyObject', req => {
|
||||
req.log.info('beforeSave MyObject', 'info log', { info: 'some log' });
|
||||
req.log.error('beforeSave MyObject', 'error log', {
|
||||
@@ -120,7 +120,7 @@ describe('Cloud Code Logger', () => {
|
||||
expect(truncatedString.length).toBe(1015); // truncate length + the string '... (truncated)'
|
||||
});
|
||||
|
||||
it('should truncate input and result of long lines', done => {
|
||||
it_id('4a009b1f-9203-49ca-8d48-5b45f4eedbdf')(it)('should truncate input and result of long lines', done => {
|
||||
const longString = fs.readFileSync(loremFile, 'utf8');
|
||||
Parse.Cloud.define('aFunction', req => {
|
||||
return req.params;
|
||||
@@ -138,7 +138,7 @@ describe('Cloud Code Logger', () => {
|
||||
.then(null, e => done.fail(e));
|
||||
});
|
||||
|
||||
it('should log an afterSave', done => {
|
||||
it_id('9857e15d-bb18-478d-8a67-fdaad3e89565')(it)('should log an afterSave', done => {
|
||||
Parse.Cloud.afterSave('MyObject', () => {});
|
||||
new Parse.Object('MyObject')
|
||||
.save()
|
||||
@@ -151,7 +151,7 @@ describe('Cloud Code Logger', () => {
|
||||
.then(null, e => done.fail(e));
|
||||
});
|
||||
|
||||
it('should log a denied beforeSave', done => {
|
||||
it_id('ec13a296-f8b1-4fc6-985a-3593462edd9c')(it)('should log a denied beforeSave', done => {
|
||||
Parse.Cloud.beforeSave('MyObject', () => {
|
||||
throw 'uh oh!';
|
||||
});
|
||||
@@ -174,7 +174,7 @@ describe('Cloud Code Logger', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should log cloud function success', done => {
|
||||
it_id('3e0caa45-60d6-41af-829a-fd389710c132')(it)('should log cloud function success', done => {
|
||||
Parse.Cloud.define('aFunction', () => {
|
||||
return 'it worked!';
|
||||
});
|
||||
@@ -189,7 +189,7 @@ describe('Cloud Code Logger', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should log cloud function execution using the custom log level', async done => {
|
||||
it_id('8088de8a-7cba-4035-8b05-4a903307e674')(it)('should log cloud function execution using the custom log level', async done => {
|
||||
Parse.Cloud.define('aFunction', () => {
|
||||
return 'it worked!';
|
||||
});
|
||||
@@ -260,7 +260,7 @@ describe('Cloud Code Logger', () => {
|
||||
expect(calls).toEqual({ beforeSave: 'warn', afterSave: undefined });
|
||||
});
|
||||
|
||||
it('should log cloud function failure', done => {
|
||||
it_id('97e0eafa-cde6-4a9a-9e53-7db98bacbc62')(it)('should log cloud function failure', done => {
|
||||
Parse.Cloud.define('aFunction', () => {
|
||||
throw 'it failed!';
|
||||
});
|
||||
@@ -287,6 +287,7 @@ describe('Cloud Code Logger', () => {
|
||||
});
|
||||
|
||||
xit('should log a changed beforeSave indicating a change', done => {
|
||||
pending('needs more work.....');
|
||||
const logController = new LoggerController(new WinstonLoggerAdapter());
|
||||
|
||||
Parse.Cloud.beforeSave('MyObject', req => {
|
||||
@@ -309,9 +310,9 @@ describe('Cloud Code Logger', () => {
|
||||
done();
|
||||
})
|
||||
.then(null, e => done.fail(JSON.stringify(e)));
|
||||
}).pend('needs more work.....');
|
||||
});
|
||||
|
||||
it('cloud function should obfuscate password', done => {
|
||||
it_id('b86e8168-8370-4730-a4ba-24ca3016ad66')(it)('cloud function should obfuscate password', done => {
|
||||
Parse.Cloud.define('testFunction', () => {
|
||||
return 'verify code success';
|
||||
});
|
||||
|
||||
@@ -642,7 +642,8 @@ describe('DefinedSchemas', () => {
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith(`Failed to run migrations: ${error.toString()}`);
|
||||
});
|
||||
it('should perform migration in parallel without failing', async () => {
|
||||
|
||||
it_id('a18bf4f2-25c8-4de3-b986-19cb1ab163b8')(it)('should perform migration in parallel without failing', async () => {
|
||||
const server = await reconfigureServer();
|
||||
const logger = require('../lib/logger').logger;
|
||||
spyOn(logger, 'error').and.callThrough();
|
||||
|
||||
+518
-643
File diff suppressed because it is too large
Load Diff
@@ -3,11 +3,9 @@ const request = require('../lib/request');
|
||||
describe('Enable express error handler', () => {
|
||||
it('should call the default handler in case of error, like updating a non existing object', async done => {
|
||||
spyOn(console, 'error');
|
||||
const parseServer = await reconfigureServer(
|
||||
Object.assign({}, defaultConfiguration, {
|
||||
enableExpressErrorHandler: true,
|
||||
})
|
||||
);
|
||||
const parseServer = await reconfigureServer({
|
||||
enableExpressErrorHandler: true,
|
||||
});
|
||||
parseServer.app.use(function (err, req, res, next) {
|
||||
expect(err.message).toBe('Object not found.');
|
||||
next(err);
|
||||
|
||||
@@ -21,11 +21,14 @@ const mockAdapter = {
|
||||
|
||||
// Small additional tests to improve overall coverage
|
||||
describe('FilesController', () => {
|
||||
it('should properly expand objects', done => {
|
||||
it('should properly expand objects with sync getFileLocation', async () => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
gridFSAdapter.getFileLocation = (config, filename) => {
|
||||
return config.mount + '/files/' + config.applicationId + '/' + encodeURIComponent(filename);
|
||||
}
|
||||
const filesController = new FilesController(gridFSAdapter);
|
||||
const result = filesController.expandFilesInObject(config, function () {});
|
||||
const result = await filesController.expandFilesInObject(config, function () { });
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
|
||||
@@ -37,12 +40,69 @@ describe('FilesController', () => {
|
||||
const anObject = {
|
||||
aFile: fullFile,
|
||||
};
|
||||
filesController.expandFilesInObject(config, anObject);
|
||||
await filesController.expandFilesInObject(config, anObject);
|
||||
expect(anObject.aFile.url).toEqual('http://an.url');
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should properly expand objects with async getFileLocation', async () => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
gridFSAdapter.getFileLocation = async (config, filename) => {
|
||||
await Promise.resolve();
|
||||
return config.mount + '/files/' + config.applicationId + '/' + encodeURIComponent(filename);
|
||||
}
|
||||
const filesController = new FilesController(gridFSAdapter);
|
||||
const result = await filesController.expandFilesInObject(config, function () { });
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
|
||||
const fullFile = {
|
||||
type: '__type',
|
||||
url: 'http://an.url',
|
||||
};
|
||||
|
||||
const anObject = {
|
||||
aFile: fullFile,
|
||||
};
|
||||
await filesController.expandFilesInObject(config, anObject);
|
||||
expect(anObject.aFile.url).toEqual('http://an.url');
|
||||
});
|
||||
|
||||
it('should call getFileLocation when config.fileKey is undefined', async () => {
|
||||
const config = {};
|
||||
const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
|
||||
const fullFile = {
|
||||
name: 'mock-name',
|
||||
__type: 'File',
|
||||
};
|
||||
gridFSAdapter.getFileLocation = jasmine.createSpy('getFileLocation').and.returnValue(Promise.resolve('mock-url'));
|
||||
const filesController = new FilesController(gridFSAdapter);
|
||||
|
||||
const anObject = { aFile: fullFile };
|
||||
await filesController.expandFilesInObject(config, anObject);
|
||||
expect(gridFSAdapter.getFileLocation).toHaveBeenCalledWith(config, fullFile.name);
|
||||
expect(anObject.aFile.url).toEqual('mock-url');
|
||||
});
|
||||
|
||||
it('should call getFileLocation when config.fileKey is defined', async () => {
|
||||
const config = { fileKey: 'mock-key' };
|
||||
const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
|
||||
const fullFile = {
|
||||
name: 'mock-name',
|
||||
__type: 'File',
|
||||
};
|
||||
gridFSAdapter.getFileLocation = jasmine.createSpy('getFileLocation').and.returnValue(Promise.resolve('mock-url'));
|
||||
const filesController = new FilesController(gridFSAdapter);
|
||||
|
||||
const anObject = { aFile: fullFile };
|
||||
await filesController.expandFilesInObject(config, anObject);
|
||||
expect(gridFSAdapter.getFileLocation).toHaveBeenCalledWith(config, fullFile.name);
|
||||
expect(anObject.aFile.url).toEqual('mock-url');
|
||||
});
|
||||
|
||||
|
||||
it_only_db('mongo')('should pass databaseOptions to GridFSBucketAdapter', async () => {
|
||||
await reconfigureServer({
|
||||
databaseURI: 'mongodb://localhost:27017/parse',
|
||||
@@ -101,7 +161,7 @@ describe('FilesController', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should add a unique hash to the file name when the preserveFileName option is false', done => {
|
||||
it('should add a unique hash to the file name when the preserveFileName option is false', async () => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
spyOn(gridFSAdapter, 'createFile');
|
||||
@@ -112,17 +172,15 @@ describe('FilesController', () => {
|
||||
preserveFileName: false,
|
||||
});
|
||||
|
||||
filesController.createFile(config, fileName);
|
||||
await filesController.createFile(config, fileName);
|
||||
|
||||
expect(gridFSAdapter.createFile).toHaveBeenCalledTimes(1);
|
||||
expect(gridFSAdapter.createFile.calls.mostRecent().args[0]).toMatch(
|
||||
`^.{32}_${regexEscapedFileName}$`
|
||||
);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should not add a unique hash to the file name when the preserveFileName option is true', done => {
|
||||
it('should not add a unique hash to the file name when the preserveFileName option is true', async () => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
spyOn(gridFSAdapter, 'createFile');
|
||||
@@ -132,12 +190,10 @@ describe('FilesController', () => {
|
||||
preserveFileName: true,
|
||||
});
|
||||
|
||||
filesController.createFile(config, fileName);
|
||||
await filesController.createFile(config, fileName);
|
||||
|
||||
expect(gridFSAdapter.createFile).toHaveBeenCalledTimes(1);
|
||||
expect(gridFSAdapter.createFile.calls.mostRecent().args[0]).toEqual(fileName);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should handle adapter without getMetadata', async () => {
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
const httpRequest = require('../lib/request'),
|
||||
HTTPResponse = require('../lib/request').HTTPResponse,
|
||||
bodyParser = require('body-parser'),
|
||||
express = require('express');
|
||||
|
||||
const port = 13371;
|
||||
@@ -10,7 +9,7 @@ const httpRequestServer = `http://localhost:${port}`;
|
||||
|
||||
function startServer(done) {
|
||||
const app = express();
|
||||
app.use(bodyParser.json({ type: '*/*' }));
|
||||
app.use(express.json({ type: '*/*' }));
|
||||
app.get('/hello', function (req, res) {
|
||||
res.json({ response: 'OK' });
|
||||
});
|
||||
|
||||
@@ -45,8 +45,12 @@ describe('Idempotency', () => {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = process.env.PARSE_SERVER_TEST_TIMEOUT || 10000;
|
||||
});
|
||||
|
||||
// Tests
|
||||
it('should enforce idempotency for cloud code function', async () => {
|
||||
it_id('e25955fd-92eb-4b22-b8b7-38980e5cb223')(it)('should enforce idempotency for cloud code function', async () => {
|
||||
let counter = 0;
|
||||
Parse.Cloud.define('myFunction', () => {
|
||||
counter++;
|
||||
@@ -69,7 +73,7 @@ describe('Idempotency', () => {
|
||||
expect(counter).toBe(1);
|
||||
});
|
||||
|
||||
it('should delete request entry after TTL', async () => {
|
||||
it_id('be2fbe16-8178-485e-9a12-6fb541096480')(it)('should delete request entry after TTL', async () => {
|
||||
let counter = 0;
|
||||
Parse.Cloud.define('myFunction', () => {
|
||||
counter++;
|
||||
@@ -119,7 +123,7 @@ describe('Idempotency', () => {
|
||||
}
|
||||
);
|
||||
|
||||
it('should enforce idempotency for cloud code jobs', async () => {
|
||||
it_id('e976d0cc-a57f-45d4-9472-b9b052db6490')(it)('should enforce idempotency for cloud code jobs', async () => {
|
||||
let counter = 0;
|
||||
Parse.Cloud.job('myJob', () => {
|
||||
counter++;
|
||||
@@ -141,7 +145,7 @@ describe('Idempotency', () => {
|
||||
expect(counter).toBe(1);
|
||||
});
|
||||
|
||||
it('should enforce idempotency for class object creation', async () => {
|
||||
it_id('7c84a3d4-e1b6-4a0d-99f1-af3cf1a6b3d8')(it)('should enforce idempotency for class object creation', async () => {
|
||||
let counter = 0;
|
||||
Parse.Cloud.afterSave('MyClass', () => {
|
||||
counter++;
|
||||
@@ -163,7 +167,7 @@ describe('Idempotency', () => {
|
||||
expect(counter).toBe(1);
|
||||
});
|
||||
|
||||
it('should enforce idempotency for user object creation', async () => {
|
||||
it_id('a030f2dd-5d21-46ac-b53d-9d714f35d72a')(it)('should enforce idempotency for user object creation', async () => {
|
||||
let counter = 0;
|
||||
Parse.Cloud.afterSave('_User', () => {
|
||||
counter++;
|
||||
@@ -189,7 +193,7 @@ describe('Idempotency', () => {
|
||||
expect(counter).toBe(1);
|
||||
});
|
||||
|
||||
it('should enforce idempotency for installation object creation', async () => {
|
||||
it_id('064c469b-091c-4ba9-9043-be461f26a3eb')(it)('should enforce idempotency for installation object creation', async () => {
|
||||
let counter = 0;
|
||||
Parse.Cloud.afterSave('_Installation', () => {
|
||||
counter++;
|
||||
@@ -215,7 +219,7 @@ describe('Idempotency', () => {
|
||||
expect(counter).toBe(1);
|
||||
});
|
||||
|
||||
it('should not interfere with calls of different request ID', async () => {
|
||||
it_id('f11670b6-fa9c-4f21-a268-ae4b6bbff7fd')(it)('should not interfere with calls of different request ID', async () => {
|
||||
let counter = 0;
|
||||
Parse.Cloud.afterSave('MyClass', () => {
|
||||
counter++;
|
||||
@@ -236,7 +240,7 @@ describe('Idempotency', () => {
|
||||
expect(counter).toBe(100);
|
||||
});
|
||||
|
||||
it('should re-throw any other error unchanged when writing request entry fails for any other reason', async () => {
|
||||
it_id('0ecd2cd2-dafb-4a2b-bb2b-9ad4c9aca777')(it)('should re-throw any other error unchanged when writing request entry fails for any other reason', async () => {
|
||||
spyOn(rest, 'create').and.rejectWith(new Parse.Error(0, 'some other error'));
|
||||
Parse.Cloud.define('myFunction', () => {});
|
||||
const params = {
|
||||
|
||||
@@ -75,7 +75,7 @@ describe_only(() => {
|
||||
/**
|
||||
* Verifies simple passwords in GET login requests with special characters are scrubbed from the verbose log
|
||||
*/
|
||||
it('does scrub simple passwords on GET login', done => {
|
||||
it_id('e36d6141-2a20-41d0-85fc-d1534c3e4bae')(it)('does scrub simple passwords on GET login', done => {
|
||||
reconfigureServer({
|
||||
verbose: true,
|
||||
}).then(function () {
|
||||
@@ -105,7 +105,7 @@ describe_only(() => {
|
||||
/**
|
||||
* Verifies complex passwords in GET login requests with special characters are scrubbed from the verbose log
|
||||
*/
|
||||
it('does scrub complex passwords on GET login', done => {
|
||||
it_id('24b277c5-250f-4a35-a449-2c8c519d4c03')(it)('does scrub complex passwords on GET login', done => {
|
||||
reconfigureServer({
|
||||
verbose: true,
|
||||
})
|
||||
@@ -139,7 +139,7 @@ describe_only(() => {
|
||||
/**
|
||||
* Verifies fields in POST login requests are NOT present in the verbose log
|
||||
*/
|
||||
it('does not have password field in POST login', done => {
|
||||
it_id('33143ec9-b32d-467c-ba65-ff2bbefdaadd')(it)('does not have password field in POST login', done => {
|
||||
reconfigureServer({
|
||||
verbose: true,
|
||||
}).then(function () {
|
||||
|
||||
+13
-25
@@ -32,7 +32,7 @@ describe('middlewares', () => {
|
||||
AppCache.del(fakeReq.body._ApplicationId);
|
||||
});
|
||||
|
||||
it('should use _ContentType if provided', done => {
|
||||
it_id('4cc18d90-1763-4725-97fa-f63fb4692fc4')(it)('should use _ContentType if provided', done => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKeyIps: ['127.0.0.1'],
|
||||
});
|
||||
@@ -46,32 +46,32 @@ describe('middlewares', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should give invalid response when keys are configured but no key supplied', () => {
|
||||
it('should give invalid response when keys are configured but no key supplied', async () => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
restAPIKey: 'restAPIKey',
|
||||
});
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
await middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
expect(fakeRes.status).toHaveBeenCalledWith(403);
|
||||
});
|
||||
|
||||
it('should give invalid response when keys are configured but supplied key is incorrect', () => {
|
||||
it('should give invalid response when keys are configured but supplied key is incorrect', async () => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
restAPIKey: 'restAPIKey',
|
||||
});
|
||||
fakeReq.headers['x-parse-rest-api-key'] = 'wrongKey';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
await middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
expect(fakeRes.status).toHaveBeenCalledWith(403);
|
||||
});
|
||||
|
||||
it('should give invalid response when keys are configured but different key is supplied', () => {
|
||||
it('should give invalid response when keys are configured but different key is supplied', async () => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
restAPIKey: 'restAPIKey',
|
||||
});
|
||||
fakeReq.headers['x-parse-client-key'] = 'clientKey';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
await middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
expect(fakeRes.status).toHaveBeenCalledWith(403);
|
||||
});
|
||||
|
||||
@@ -128,7 +128,7 @@ describe('middlewares', () => {
|
||||
const otherKeys = BodyKeys.filter(
|
||||
otherKey => otherKey !== infoKey && otherKey !== 'javascriptKey'
|
||||
);
|
||||
it(`it should pull ${bodyKey} into req.info`, done => {
|
||||
it_id('f9abd7ac-b1f4-4607-b9b0-365ff0559d84')(it)(`it should pull ${bodyKey} into req.info`, done => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKeyIps: ['0.0.0.0/0'],
|
||||
});
|
||||
@@ -147,7 +147,7 @@ describe('middlewares', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should not succeed and log if the ip does not belong to masterKeyIps list', async () => {
|
||||
it_id('4a0bce41-c536-4482-a873-12ed023380e2')(it)('should not succeed and log if the ip does not belong to masterKeyIps list', async () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
spyOn(logger, 'error').and.callFake(() => {});
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
@@ -157,13 +157,7 @@ describe('middlewares', () => {
|
||||
fakeReq.ip = '127.0.0.1';
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
|
||||
let error;
|
||||
|
||||
try {
|
||||
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
const error = await middlewares.handleParseHeaders(fakeReq, fakeRes, () => {}).catch(e => e);
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toEqual(`unauthorized`);
|
||||
@@ -182,13 +176,7 @@ describe('middlewares', () => {
|
||||
fakeReq.ip = '10.0.0.2';
|
||||
fakeReq.headers['x-parse-maintenance-key'] = 'masterKey';
|
||||
|
||||
let error;
|
||||
|
||||
try {
|
||||
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
const error = await middlewares.handleParseHeaders(fakeReq, fakeRes, () => {}).catch(e => e);
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toEqual(`unauthorized`);
|
||||
@@ -197,7 +185,7 @@ describe('middlewares', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should succeed if the ip does belong to masterKeyIps list', async () => {
|
||||
it_id('2f7fadec-a87c-4626-90d1-65c75653aea9')(it)('should succeed if the ip does belong to masterKeyIps list', async () => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['10.0.0.1'],
|
||||
@@ -208,7 +196,7 @@ describe('middlewares', () => {
|
||||
expect(fakeReq.auth.isMaster).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow any ip to use masterKey if masterKeyIps is empty', async () => {
|
||||
it_id('2b251fd4-d43c-48f4-ada9-c8458e40c12a')(it)('should allow any ip to use masterKey if masterKeyIps is empty', async () => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['0.0.0.0/0'],
|
||||
|
||||
@@ -18,6 +18,12 @@ describe('MongoSchemaCollection', () => {
|
||||
},
|
||||
_metadata: {
|
||||
class_permissions: {
|
||||
ACL: {
|
||||
'*': {
|
||||
read: true,
|
||||
write: true,
|
||||
},
|
||||
},
|
||||
get: { '*': true },
|
||||
find: { '*': true },
|
||||
count: { '*': true },
|
||||
@@ -69,6 +75,12 @@ describe('MongoSchemaCollection', () => {
|
||||
objectId: { type: 'String' },
|
||||
},
|
||||
classLevelPermissions: {
|
||||
ACL: {
|
||||
'*': {
|
||||
read: true,
|
||||
write: true,
|
||||
},
|
||||
},
|
||||
find: { '*': true },
|
||||
get: { '*': true },
|
||||
count: { '*': true },
|
||||
|
||||
@@ -15,8 +15,8 @@ const fakeClient = {
|
||||
// These tests are specific to the mongo storage adapter + mongo storage format
|
||||
// and will eventually be moved into their own repo
|
||||
describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
beforeEach(done => {
|
||||
new MongoStorageAdapter({ uri: databaseURI }).deleteAllClasses().then(done, fail);
|
||||
beforeEach(async () => {
|
||||
await new MongoStorageAdapter({ uri: databaseURI }).deleteAllClasses();
|
||||
Config.get(Parse.applicationId).schemaCache.clear();
|
||||
});
|
||||
|
||||
@@ -390,7 +390,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
await expectAsync(adapter.getClass('UnknownClass')).toBeRejectedWith(undefined);
|
||||
});
|
||||
|
||||
it_only_mongodb_version('<5.1>=6')('should use index for caseInsensitive query', async () => {
|
||||
it_only_mongodb_version('<5.1 || >=6')('should use index for caseInsensitive query', async () => {
|
||||
const user = new Parse.User();
|
||||
user.set('username', 'Bugs');
|
||||
user.set('password', 'Bunny');
|
||||
@@ -424,40 +424,6 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
expect(postIndexPlan.executionStats.executionStages.stage).toBe('FETCH');
|
||||
});
|
||||
|
||||
it_only_mongodb_version('>=5.1<6')('should use index for caseInsensitive query', async () => {
|
||||
const user = new Parse.User();
|
||||
user.set('username', 'Bugs');
|
||||
user.set('password', 'Bunny');
|
||||
await user.signUp();
|
||||
|
||||
const database = Config.get(Parse.applicationId).database;
|
||||
await database.adapter.dropAllIndexes('_User');
|
||||
|
||||
const preIndexPlan = await database.find(
|
||||
'_User',
|
||||
{ username: 'bugs' },
|
||||
{ caseInsensitive: true, explain: true }
|
||||
);
|
||||
|
||||
const schema = await new Parse.Schema('_User').get();
|
||||
|
||||
await database.adapter.ensureIndex(
|
||||
'_User',
|
||||
schema,
|
||||
['username'],
|
||||
'case_insensitive_username',
|
||||
true
|
||||
);
|
||||
|
||||
const postIndexPlan = await database.find(
|
||||
'_User',
|
||||
{ username: 'bugs' },
|
||||
{ caseInsensitive: true, explain: true }
|
||||
);
|
||||
expect(preIndexPlan.queryPlanner.winningPlan.queryPlan.stage).toBe('COLLSCAN');
|
||||
expect(postIndexPlan.queryPlanner.winningPlan.queryPlan.stage).toBe('FETCH');
|
||||
});
|
||||
|
||||
it('should delete field without index', async () => {
|
||||
const database = Config.get(Parse.applicationId).database;
|
||||
const obj = new Parse.Object('MyObject');
|
||||
|
||||
+14
-42
@@ -108,7 +108,7 @@ describe('Pages Router', () => {
|
||||
const res = await request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: `new_password=user1&token=43634643&username=username`,
|
||||
body: `new_password=user1&token=43634643`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
@@ -124,7 +124,7 @@ describe('Pages Router', () => {
|
||||
await request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: `new_password=&token=132414&username=Johnny`,
|
||||
body: `new_password=&token=132414`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
@@ -137,30 +137,12 @@ describe('Pages Router', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('request_password_reset: responds with AJAX error on missing username', async () => {
|
||||
try {
|
||||
await request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: `new_password=user1&token=43634643&username=`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
followRedirects: false,
|
||||
});
|
||||
} catch (error) {
|
||||
expect(error.status).not.toBe(302);
|
||||
expect(error.text).toEqual('{"code":200,"error":"Missing username"}');
|
||||
}
|
||||
});
|
||||
|
||||
it('request_password_reset: responds with AJAX error on missing token', async () => {
|
||||
try {
|
||||
await request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: `new_password=user1&token=&username=Johnny`,
|
||||
body: `new_password=user1&token=`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
@@ -577,7 +559,7 @@ describe('Pages Router', () => {
|
||||
spyOnProperty(Page.prototype, 'defaultFile').and.returnValue(jsonPageFile);
|
||||
|
||||
const response = await request({
|
||||
url: `http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&username=exampleUsername&locale=${exampleLocale}`,
|
||||
url: `http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&locale=${exampleLocale}`,
|
||||
followRedirects: false,
|
||||
}).catch(e => e);
|
||||
expect(response.status).toEqual(200);
|
||||
@@ -626,7 +608,7 @@ describe('Pages Router', () => {
|
||||
await reconfigureServer(config);
|
||||
const response = await request({
|
||||
url:
|
||||
'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&username=exampleUsername&locale=de-AT',
|
||||
'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&locale=de-AT',
|
||||
followRedirects: false,
|
||||
method: 'POST',
|
||||
});
|
||||
@@ -640,7 +622,7 @@ describe('Pages Router', () => {
|
||||
await reconfigureServer(config);
|
||||
const response = await request({
|
||||
url:
|
||||
'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&username=exampleUsername&locale=de-AT',
|
||||
'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&locale=de-AT',
|
||||
followRedirects: false,
|
||||
method: 'GET',
|
||||
});
|
||||
@@ -676,13 +658,11 @@ describe('Pages Router', () => {
|
||||
const appId = linkResponse.headers['x-parse-page-param-appid'];
|
||||
const token = linkResponse.headers['x-parse-page-param-token'];
|
||||
const locale = linkResponse.headers['x-parse-page-param-locale'];
|
||||
const username = linkResponse.headers['x-parse-page-param-username'];
|
||||
const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl'];
|
||||
const passwordResetPagePath = pageResponse.calls.all()[0].args[0];
|
||||
expect(appId).toBeDefined();
|
||||
expect(token).toBeDefined();
|
||||
expect(locale).toBeDefined();
|
||||
expect(username).toBeDefined();
|
||||
expect(publicServerUrl).toBeDefined();
|
||||
expect(passwordResetPagePath).toMatch(
|
||||
new RegExp(`\/${exampleLocale}\/${pages.passwordReset.defaultFile}`)
|
||||
@@ -696,7 +676,6 @@ describe('Pages Router', () => {
|
||||
body: {
|
||||
token,
|
||||
locale,
|
||||
username,
|
||||
new_password: 'newPassword',
|
||||
},
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
@@ -738,7 +717,7 @@ describe('Pages Router', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('localizes end-to-end for verify email: success', async () => {
|
||||
it_id('2845c2ea-23ba-45d2-a33f-63181d419bca')(it)('localizes end-to-end for verify email: success', async () => {
|
||||
await reconfigureServer(config);
|
||||
const sendVerificationEmail = spyOn(
|
||||
config.emailAdapter,
|
||||
@@ -767,7 +746,7 @@ describe('Pages Router', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('localizes end-to-end for verify email: invalid verification link - link send success', async () => {
|
||||
it_id('f2272b94-b4ac-474f-8e47-1ca74de136f5')(it)('localizes end-to-end for verify email: invalid verification link - link send success', async () => {
|
||||
await reconfigureServer(config);
|
||||
const sendVerificationEmail = spyOn(
|
||||
config.emailAdapter,
|
||||
@@ -793,15 +772,13 @@ describe('Pages Router', () => {
|
||||
|
||||
const appId = linkResponse.headers['x-parse-page-param-appid'];
|
||||
const locale = linkResponse.headers['x-parse-page-param-locale'];
|
||||
const username = linkResponse.headers['x-parse-page-param-username'];
|
||||
const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl'];
|
||||
const invalidVerificationPagePath = pageResponse.calls.all()[0].args[0];
|
||||
expect(appId).toBeDefined();
|
||||
expect(locale).toBe(exampleLocale);
|
||||
expect(username).toBeDefined();
|
||||
expect(publicServerUrl).toBeDefined();
|
||||
expect(invalidVerificationPagePath).toMatch(
|
||||
new RegExp(`\/${exampleLocale}\/${pages.emailVerificationLinkExpired.defaultFile}`)
|
||||
new RegExp(`\/${exampleLocale}\/${pages.emailVerificationLinkInvalid.defaultFile}`)
|
||||
);
|
||||
|
||||
const formUrl = `${publicServerUrl}/apps/${appId}/resend_verification_email`;
|
||||
@@ -810,7 +787,7 @@ describe('Pages Router', () => {
|
||||
method: 'POST',
|
||||
body: {
|
||||
locale,
|
||||
username,
|
||||
username: 'exampleUsername',
|
||||
},
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
followRedirects: false,
|
||||
@@ -821,7 +798,7 @@ describe('Pages Router', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('localizes end-to-end for verify email: invalid verification link - link send fail', async () => {
|
||||
it_id('1d46d36a-e455-4ae7-8717-e0d286e95f02')(it)('localizes end-to-end for verify email: invalid verification link - link send fail', async () => {
|
||||
await reconfigureServer(config);
|
||||
const sendVerificationEmail = spyOn(
|
||||
config.emailAdapter,
|
||||
@@ -847,17 +824,15 @@ describe('Pages Router', () => {
|
||||
|
||||
const appId = linkResponse.headers['x-parse-page-param-appid'];
|
||||
const locale = linkResponse.headers['x-parse-page-param-locale'];
|
||||
const username = linkResponse.headers['x-parse-page-param-username'];
|
||||
const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl'];
|
||||
await jasmine.timeout();
|
||||
|
||||
const invalidVerificationPagePath = pageResponse.calls.all()[0].args[0];
|
||||
expect(appId).toBeDefined();
|
||||
expect(locale).toBe(exampleLocale);
|
||||
expect(username).toBeDefined();
|
||||
expect(publicServerUrl).toBeDefined();
|
||||
expect(invalidVerificationPagePath).toMatch(
|
||||
new RegExp(`\/${exampleLocale}\/${pages.emailVerificationLinkExpired.defaultFile}`)
|
||||
new RegExp(`\/${exampleLocale}\/${pages.emailVerificationLinkInvalid.defaultFile}`)
|
||||
);
|
||||
|
||||
spyOn(UserController.prototype, 'resendVerificationEmail').and.callFake(() =>
|
||||
@@ -870,7 +845,7 @@ describe('Pages Router', () => {
|
||||
method: 'POST',
|
||||
body: {
|
||||
locale,
|
||||
username,
|
||||
username: 'exampleUsername',
|
||||
},
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
followRedirects: false,
|
||||
@@ -1155,12 +1130,10 @@ describe('Pages Router', () => {
|
||||
|
||||
const appId = linkResponse.headers['x-parse-page-param-appid'];
|
||||
const token = linkResponse.headers['x-parse-page-param-token'];
|
||||
const username = linkResponse.headers['x-parse-page-param-username'];
|
||||
const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl'];
|
||||
const passwordResetPagePath = pageResponse.calls.all()[0].args[0];
|
||||
expect(appId).toBeDefined();
|
||||
expect(token).toBeDefined();
|
||||
expect(username).toBeDefined();
|
||||
expect(publicServerUrl).toBeDefined();
|
||||
expect(passwordResetPagePath).toMatch(new RegExp(`\/${pages.passwordReset.defaultFile}`));
|
||||
pageResponse.calls.reset();
|
||||
@@ -1171,7 +1144,6 @@ describe('Pages Router', () => {
|
||||
method: 'POST',
|
||||
body: {
|
||||
token,
|
||||
username,
|
||||
new_password: 'newPassword',
|
||||
},
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
@@ -1183,7 +1155,7 @@ describe('Pages Router', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('email verification works with custom endpoint', async () => {
|
||||
it_id('81c1c28e-5dfd-4ffb-a09b-283156c08483')(it)('email verification works with custom endpoint', async () => {
|
||||
config.pages.pagesEndpoint = 'customEndpoint';
|
||||
await reconfigureServer(config);
|
||||
const sendVerificationEmail = spyOn(
|
||||
|
||||
@@ -111,7 +111,7 @@ const setup = function () {
|
||||
};
|
||||
|
||||
describe('Parse.Push', () => {
|
||||
it('should properly send push', async () => {
|
||||
it_id('d1e591c4-2b21-466b-9ee2-5be467b6b771')(it)('should properly send push', async () => {
|
||||
const { sendToInstallationSpy } = await setup();
|
||||
const pushStatusId = await Parse.Push.send({
|
||||
where: {
|
||||
@@ -126,7 +126,7 @@ describe('Parse.Push', () => {
|
||||
expect(sendToInstallationSpy.calls.count()).toEqual(10);
|
||||
});
|
||||
|
||||
it('should properly send push with lowercaseIncrement', async () => {
|
||||
it_id('2a58e3c7-b6f3-4261-a384-6c893b2ac3f3')(it)('should properly send push with lowercaseIncrement', async () => {
|
||||
await setup();
|
||||
const pushStatusId = await Parse.Push.send({
|
||||
where: {
|
||||
@@ -140,7 +140,7 @@ describe('Parse.Push', () => {
|
||||
await pushCompleted(pushStatusId);
|
||||
});
|
||||
|
||||
it('should not allow clients to query _PushStatus', async () => {
|
||||
it_id('e21780b6-2cdd-467e-8013-81030f3288e9')(it)('should not allow clients to query _PushStatus', async () => {
|
||||
await setup();
|
||||
const pushStatusId = await Parse.Push.send({
|
||||
where: {
|
||||
@@ -166,7 +166,7 @@ describe('Parse.Push', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should allow master key to query _PushStatus', async () => {
|
||||
it_id('924cf5f5-f684-4925-978a-e52c0c457366')(it)('should allow master key to query _PushStatus', async () => {
|
||||
await setup();
|
||||
const pushStatusId = await Parse.Push.send({
|
||||
where: {
|
||||
|
||||
+30
-10
@@ -5,17 +5,9 @@ const Config = require('../lib/Config');
|
||||
const auth = require('../lib/Auth');
|
||||
|
||||
describe('Parse.ACL', () => {
|
||||
it('acl must be valid', done => {
|
||||
it('acl must be valid', () => {
|
||||
const user = new Parse.User();
|
||||
ok(
|
||||
!user.setACL("Ceci n'est pas un ACL.", {
|
||||
error: function (user, error) {
|
||||
equal(error.code, -1);
|
||||
done();
|
||||
},
|
||||
}),
|
||||
'setACL should have returned false.'
|
||||
);
|
||||
expect(() => user.setACL('ACL')).toThrow(new Parse.Error(Parse.Error.OTHER_CAUSE, 'ACL must be a Parse ACL.'));
|
||||
});
|
||||
|
||||
it('refresh object with acl', async done => {
|
||||
@@ -931,4 +923,32 @@ describe('Parse.ACL', () => {
|
||||
|
||||
rest.create(config, auth.nobody(config), '_User', anonUser);
|
||||
});
|
||||
|
||||
it('support defaultACL in schema', async () => {
|
||||
await new Parse.Object('TestObject').save();
|
||||
const schema = await Parse.Server.database.loadSchema();
|
||||
await schema.updateClass(
|
||||
'TestObject',
|
||||
{},
|
||||
{
|
||||
create: {
|
||||
'*': true,
|
||||
},
|
||||
ACL: {
|
||||
'*': { read: true },
|
||||
currentUser: { read: true, write: true },
|
||||
},
|
||||
}
|
||||
);
|
||||
const acls = new Parse.ACL();
|
||||
acls.setPublicReadAccess(true);
|
||||
const user = await Parse.User.signUp('testuser', 'p@ssword');
|
||||
const obj = new Parse.Object('TestObject');
|
||||
await obj.save(null, { sessionToken: user.getSessionToken() });
|
||||
expect(obj.getACL()).toBeDefined();
|
||||
const acl = obj.getACL().toJSON();
|
||||
expect(acl['*']).toEqual({ read: true });
|
||||
expect(acl[user.id].write).toBeTrue();
|
||||
expect(acl[user.id].read).toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
+8
-10
@@ -33,9 +33,7 @@ describe('miscellaneous', () => {
|
||||
expect(results.length).toEqual(1);
|
||||
expect(results[0]['foo']).toEqual('bar');
|
||||
});
|
||||
});
|
||||
|
||||
describe('miscellaneous', function () {
|
||||
it('create a GameScore object', function (done) {
|
||||
const obj = new Parse.Object('GameScore');
|
||||
obj.set('score', 1337);
|
||||
@@ -163,7 +161,7 @@ describe('miscellaneous', function () {
|
||||
expect(numCreated).toBe(1);
|
||||
});
|
||||
|
||||
it('ensure that if people already have duplicate users, they can still sign up new users', async done => {
|
||||
it_id('be1b9ac7-5e5f-4e91-b044-2bd8fb7622ad')(it)('ensure that if people already have duplicate users, they can still sign up new users', async done => {
|
||||
try {
|
||||
await Parse.User.logOut();
|
||||
} catch (e) {
|
||||
@@ -209,7 +207,7 @@ describe('miscellaneous', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('ensure that if people already have duplicate emails, they can still sign up new users', done => {
|
||||
it_id('d00f907e-41b9-40f6-8168-63e832199a8c')(it)('ensure that if people already have duplicate emails, they can still sign up new users', done => {
|
||||
const config = Config.get('test');
|
||||
// Remove existing data to clear out unique index
|
||||
TestUtils.destroyAllDataPermanently()
|
||||
@@ -289,7 +287,7 @@ describe('miscellaneous', function () {
|
||||
}, fail);
|
||||
});
|
||||
|
||||
it('increment with a user object', function (done) {
|
||||
it_id('33db6efe-7c02-496c-8595-0ef627a94103')(it)('increment with a user object', function (done) {
|
||||
createTestUser()
|
||||
.then(user => {
|
||||
user.increment('foo');
|
||||
@@ -317,7 +315,7 @@ describe('miscellaneous', function () {
|
||||
);
|
||||
});
|
||||
|
||||
it('save various data types', function (done) {
|
||||
it_id('bef99522-bcfd-4f79-ba9e-3c3845550401')(it)('save various data types', function (done) {
|
||||
const obj = new TestObject();
|
||||
obj.set('date', new Date());
|
||||
obj.set('array', [1, 2, 3]);
|
||||
@@ -951,7 +949,7 @@ describe('miscellaneous', function () {
|
||||
);
|
||||
});
|
||||
|
||||
it('return the updated fields on PUT', async () => {
|
||||
it_id('e9e718a9-4465-4158-b13e-f146855a8892')(it)('return the updated fields on PUT', async () => {
|
||||
const obj = new Parse.Object('GameScore');
|
||||
const pointer = new Parse.Object('Child');
|
||||
await pointer.save();
|
||||
@@ -1023,7 +1021,7 @@ describe('miscellaneous', function () {
|
||||
expect(body.updatedAt).not.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should response should not change with triggers', async () => {
|
||||
it_id('ea358b59-03c0-45c9-abc7-1fdd67573029')(it)('should response should not change with triggers', async () => {
|
||||
const obj = new Parse.Object('GameScore');
|
||||
const pointer = new Parse.Object('Child');
|
||||
Parse.Cloud.beforeSave('GameScore', request => {
|
||||
@@ -1491,7 +1489,7 @@ describe('miscellaneous', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('properly returns incremented values (#1554)', done => {
|
||||
it_id('b2cd9cf2-13fa-4acd-aaa9-6f81fc1858db')(it)('properly returns incremented values (#1554)', done => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
@@ -1680,7 +1678,7 @@ describe('miscellaneous', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('purge all objects in class', done => {
|
||||
it_id('8f99ee20-3da7-45ec-b867-ea0eb87524a9')(it)('purge all objects in class', done => {
|
||||
const object = new Parse.Object('TestObject');
|
||||
object.set('foo', 'bar');
|
||||
const object2 = new Parse.Object('TestObject');
|
||||
|
||||
@@ -12,7 +12,6 @@ describe('Config Keys', () => {
|
||||
|
||||
it('recognizes invalid keys in root', async () => {
|
||||
await expectAsync(reconfigureServer({
|
||||
...defaultConfiguration,
|
||||
invalidKey: 1,
|
||||
})).toBeResolved();
|
||||
const error = loggerErrorSpy.calls.all().reduce((s, call) => s += call.args[0], '');
|
||||
@@ -21,7 +20,6 @@ describe('Config Keys', () => {
|
||||
|
||||
it('recognizes invalid keys in pages.customUrls', async () => {
|
||||
await expectAsync(reconfigureServer({
|
||||
...defaultConfiguration,
|
||||
pages: {
|
||||
customUrls: {
|
||||
invalidKey: 1,
|
||||
@@ -37,7 +35,6 @@ describe('Config Keys', () => {
|
||||
|
||||
it('recognizes invalid keys in liveQueryServerOptions', async () => {
|
||||
await expectAsync(reconfigureServer({
|
||||
...defaultConfiguration,
|
||||
liveQueryServerOptions: {
|
||||
invalidKey: 1,
|
||||
MasterKey: 1,
|
||||
@@ -50,7 +47,6 @@ describe('Config Keys', () => {
|
||||
|
||||
it('recognizes invalid keys in rateLimit', async () => {
|
||||
await expectAsync(reconfigureServer({
|
||||
...defaultConfiguration,
|
||||
rateLimit: [
|
||||
{ invalidKey: 1 },
|
||||
{ RequestPath: 1 },
|
||||
@@ -64,7 +60,7 @@ describe('Config Keys', () => {
|
||||
expect(error).toMatch('rateLimit\\[2\\]\\.RequestTimeWindow');
|
||||
});
|
||||
|
||||
it('recognizes valid keys in default configuration', async () => {
|
||||
it_only_db('mongo')('recognizes valid keys in default configuration', async () => {
|
||||
await expectAsync(reconfigureServer({
|
||||
...defaultConfiguration,
|
||||
})).toBeResolved();
|
||||
@@ -81,6 +77,11 @@ describe('Config Keys', () => {
|
||||
maxTimeMS: 1000,
|
||||
maxStalenessSeconds: 10,
|
||||
maxPoolSize: 10,
|
||||
minPoolSize: 5,
|
||||
connectTimeoutMS: 5000,
|
||||
socketTimeoutMS: 5000,
|
||||
autoSelectFamily: true,
|
||||
autoSelectFamilyAttemptTimeout: 3000
|
||||
},
|
||||
})).toBeResolved();
|
||||
expect(loggerErrorSpy.calls.all().reduce((s, call) => s += call.args[0], '')).not.toMatch(invalidKeyErrorMessage);
|
||||
|
||||
@@ -378,6 +378,26 @@ describe('Parse.File testing', () => {
|
||||
expect(response.headers['content-type']).toMatch(/^text\/html/);
|
||||
});
|
||||
|
||||
it('works without Content-Type and extension', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
},
|
||||
});
|
||||
const headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
const result = await request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/files/file',
|
||||
body: '<html></html>\n',
|
||||
});
|
||||
expect(result.data.url.includes('file.txt')).toBeTrue();
|
||||
expect(result.data.name.includes('file.txt')).toBeTrue();
|
||||
});
|
||||
|
||||
it('filename is url encoded', done => {
|
||||
const headers = {
|
||||
'Content-Type': 'text/html',
|
||||
|
||||
+11
-11
@@ -93,7 +93,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('geo line', async done => {
|
||||
it_id('bbd9e2f6-7f61-458f-98f2-4a563586cd8d')(it)('geo line', async done => {
|
||||
const line = [];
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
const obj = new TestObject();
|
||||
@@ -143,7 +143,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('geo max distance medium', async () => {
|
||||
it_id('e1e86b38-b8a4-4109-8330-a324fe628e0c')(it)('geo max distance medium', async () => {
|
||||
const objects = [];
|
||||
[0, 1, 2].map(function (i) {
|
||||
const obj = new TestObject();
|
||||
@@ -207,7 +207,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('geo max distance in km california', async () => {
|
||||
it_id('05f1a454-56b1-4f2e-908e-408a9222cbae')(it)('geo max distance in km california', async () => {
|
||||
await makeSomeGeoPoints();
|
||||
const sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
|
||||
const query = new Parse.Query(TestObject);
|
||||
@@ -246,7 +246,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
equal(results.length, 3);
|
||||
});
|
||||
|
||||
it('geo max distance in miles california', async () => {
|
||||
it_id('9ee376ad-dd6c-4c17-ad28-c7899a4411f1')(it)('geo max distance in miles california', async () => {
|
||||
await makeSomeGeoPoints();
|
||||
const sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
|
||||
const query = new Parse.Query(TestObject);
|
||||
@@ -276,7 +276,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
equal(results.length, 0);
|
||||
});
|
||||
|
||||
it('returns nearest location', async () => {
|
||||
it_id('9e35a89e-bc2c-4ec5-b25a-8d1890a55233')(it)('returns nearest location', async () => {
|
||||
await makeSomeGeoPoints();
|
||||
const sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
|
||||
const query = new Parse.Query(TestObject);
|
||||
@@ -286,7 +286,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
equal(results[1].get('name'), 'Sacramento');
|
||||
});
|
||||
|
||||
it('works with geobox queries', done => {
|
||||
it_id('6df434b0-142d-4302-bbc6-a6ec5a9d9c68')(it)('works with geobox queries', done => {
|
||||
const inbound = new Parse.GeoPoint(1.5, 1.5);
|
||||
const onbound = new Parse.GeoPoint(10, 10);
|
||||
const outbound = new Parse.GeoPoint(20, 20);
|
||||
@@ -356,7 +356,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('supports withinPolygon open path', done => {
|
||||
it_id('d9fbc5c6-f767-47d6-bb44-3858eb9df15a')(it)('supports withinPolygon open path', done => {
|
||||
const inbound = new Parse.GeoPoint(1.5, 1.5);
|
||||
const onbound = new Parse.GeoPoint(10, 10);
|
||||
const outbound = new Parse.GeoPoint(20, 20);
|
||||
@@ -394,7 +394,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
}, done.fail);
|
||||
});
|
||||
|
||||
it('supports withinPolygon closed path', done => {
|
||||
it_id('3ec537bd-839a-4c93-a48b-b4a249820074')(it)('supports withinPolygon closed path', done => {
|
||||
const inbound = new Parse.GeoPoint(1.5, 1.5);
|
||||
const onbound = new Parse.GeoPoint(10, 10);
|
||||
const outbound = new Parse.GeoPoint(20, 20);
|
||||
@@ -433,7 +433,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
}, done.fail);
|
||||
});
|
||||
|
||||
it('supports withinPolygon Polygon object', done => {
|
||||
it_id('0a248e11-3598-480a-9ab5-8a0b259258e4')(it)('supports withinPolygon Polygon object', done => {
|
||||
const inbound = new Parse.GeoPoint(1.5, 1.5);
|
||||
const onbound = new Parse.GeoPoint(10, 10);
|
||||
const outbound = new Parse.GeoPoint(20, 20);
|
||||
@@ -752,7 +752,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
equal(count, 1);
|
||||
});
|
||||
|
||||
it('withinKilometers complex supports count', async () => {
|
||||
it_id('0b073d31-0d41-41e7-bd60-f636ffb759dc')(it)('withinKilometers complex supports count', async () => {
|
||||
const inside = new Parse.GeoPoint(10, 10);
|
||||
const middle = new Parse.GeoPoint(20, 20);
|
||||
const outside = new Parse.GeoPoint(30, 30);
|
||||
@@ -770,7 +770,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
equal(count, 2);
|
||||
});
|
||||
|
||||
it('fails to fetch geopoints that are specifically not at (0,0)', async () => {
|
||||
it_id('26c9a13d-3d71-452e-a91c-9a4589be021c')(it)('fails to fetch geopoints that are specifically not at (0,0)', async () => {
|
||||
const tmp = new TestObject({
|
||||
location: new Parse.GeoPoint({ latitude: 0, longitude: 0 }),
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ const request = require('../lib/request');
|
||||
const Config = require('../lib/Config');
|
||||
|
||||
describe('a GlobalConfig', () => {
|
||||
beforeEach(done => {
|
||||
beforeEach(async () => {
|
||||
const config = Config.get('test');
|
||||
const query = on_db(
|
||||
'mongo',
|
||||
@@ -16,7 +16,7 @@ describe('a GlobalConfig', () => {
|
||||
return { objectId: '1' };
|
||||
}
|
||||
);
|
||||
config.database.adapter
|
||||
await config.database.adapter
|
||||
.upsertOneObject(
|
||||
'_GlobalConfig',
|
||||
{
|
||||
@@ -31,11 +31,7 @@ describe('a GlobalConfig', () => {
|
||||
params: { companies: ['US', 'DK'], counter: 20, internalParam: 'internal' },
|
||||
masterKeyOnly: { internalParam: true },
|
||||
}
|
||||
)
|
||||
.then(done, err => {
|
||||
jfail(err);
|
||||
done();
|
||||
});
|
||||
);
|
||||
});
|
||||
|
||||
const headers = {
|
||||
@@ -185,7 +181,7 @@ describe('a GlobalConfig', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('properly handles delete op', done => {
|
||||
it_id('5ebbd0cf-d1a5-49d9-aac7-5216abc5cb62')(it)('properly handles delete op', done => {
|
||||
request({
|
||||
method: 'PUT',
|
||||
url: 'http://localhost:8378/1/config',
|
||||
|
||||
@@ -500,7 +500,7 @@ describe('ParseGraphQLSchema', () => {
|
||||
});
|
||||
});
|
||||
describe('alias', () => {
|
||||
it('Should be able to define alias for get and find query', async () => {
|
||||
it_id('45282d26-f4c7-4d2d-a7b6-cd8741d5322f')(it)('Should be able to define alias for get and find query', async () => {
|
||||
const parseGraphQLSchema = new ParseGraphQLSchema({
|
||||
databaseController,
|
||||
parseGraphQLController,
|
||||
@@ -535,7 +535,7 @@ describe('ParseGraphQLSchema', () => {
|
||||
expect(Object.keys(queries1)).toContain('precious_data');
|
||||
});
|
||||
|
||||
it('Should be able to define alias for mutation', async () => {
|
||||
it_id('f04b46e3-a25d-401d-a315-3298cfee1df8')(it)('Should be able to define alias for mutation', async () => {
|
||||
const parseGraphQLSchema = new ParseGraphQLSchema({
|
||||
databaseController,
|
||||
parseGraphQLController,
|
||||
|
||||
+181
-55
@@ -9,7 +9,7 @@ const { updateCLP } = require('./support/dev');
|
||||
|
||||
const pluralize = require('pluralize');
|
||||
const { getMainDefinition } = require('@apollo/client/utilities');
|
||||
const { createUploadLink } = require('apollo-upload-client');
|
||||
const createUploadLink = (...args) => import('apollo-upload-client/createUploadLink.mjs').then(({ default: fn }) => fn(...args));
|
||||
const { SubscriptionClient } = require('subscriptions-transport-ws');
|
||||
const { WebSocketLink } = require('@apollo/client/link/ws');
|
||||
const { mergeSchemas } = require('@graphql-tools/schema');
|
||||
@@ -50,6 +50,7 @@ describe('ParseGraphQLServer', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
parseServer = await global.reconfigureServer({
|
||||
maintenanceKey: 'test2',
|
||||
maxUploadSize: '1kb',
|
||||
});
|
||||
parseGraphQLServer = new ParseGraphQLServer(parseServer, {
|
||||
@@ -88,8 +89,8 @@ describe('ParseGraphQLServer', () => {
|
||||
|
||||
it('should initialize parseGraphQLSchema with a log controller', async () => {
|
||||
const loggerAdapter = {
|
||||
log: () => {},
|
||||
error: () => {},
|
||||
log: () => { },
|
||||
error: () => { },
|
||||
};
|
||||
const parseServer = await global.reconfigureServer({
|
||||
loggerAdapter,
|
||||
@@ -124,13 +125,13 @@ describe('ParseGraphQLServer', () => {
|
||||
info: new Object(),
|
||||
config: new Object(),
|
||||
auth: new Object(),
|
||||
get: () => {},
|
||||
get: () => { },
|
||||
};
|
||||
const res = {
|
||||
set: () => {},
|
||||
set: () => { },
|
||||
};
|
||||
|
||||
it("should return schema and context with req's info, config and auth", async () => {
|
||||
it_id('0696675e-060f-414f-bc77-9d57f31807f5')(it)('should return schema and context with req\'s info, config and auth', async () => {
|
||||
const options = await parseGraphQLServer._getGraphQLOptions();
|
||||
expect(options.schema).toEqual(parseGraphQLServer.parseGraphQLSchema.graphQLSchema);
|
||||
const contextResponse = await options.context({ req, res });
|
||||
@@ -431,17 +432,33 @@ describe('ParseGraphQLServer', () => {
|
||||
objects.push(object1, object2, object3, object4);
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
async function createGQLFromParseServer(_parseServer, parseGraphQLServerOptions) {
|
||||
if (parseLiveQueryServer) {
|
||||
await parseLiveQueryServer.server.close();
|
||||
}
|
||||
if (httpServer) {
|
||||
await httpServer.close();
|
||||
}
|
||||
const expressApp = express();
|
||||
httpServer = http.createServer(expressApp);
|
||||
expressApp.use('/parse', parseServer.app);
|
||||
expressApp.use('/parse', _parseServer.app);
|
||||
parseLiveQueryServer = await ParseServer.createLiveQueryServer(httpServer, {
|
||||
port: 1338,
|
||||
});
|
||||
parseGraphQLServer = new ParseGraphQLServer(_parseServer, {
|
||||
graphQLPath: '/graphql',
|
||||
playgroundPath: '/playground',
|
||||
subscriptionsPath: '/subscriptions',
|
||||
...parseGraphQLServerOptions,
|
||||
});
|
||||
parseGraphQLServer.applyGraphQL(expressApp);
|
||||
parseGraphQLServer.applyPlayground(expressApp);
|
||||
parseGraphQLServer.createSubscriptions(httpServer);
|
||||
await new Promise(resolve => httpServer.listen({ port: 13377 }, resolve));
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await createGQLFromParseServer(parseServer);
|
||||
|
||||
const subscriptionClient = new SubscriptionClient(
|
||||
'ws://localhost:13377/subscriptions',
|
||||
@@ -452,7 +469,7 @@ describe('ParseGraphQLServer', () => {
|
||||
ws
|
||||
);
|
||||
const wsLink = new WebSocketLink(subscriptionClient);
|
||||
const httpLink = createUploadLink({
|
||||
const httpLink = await createUploadLink({
|
||||
uri: 'http://localhost:13377/graphql',
|
||||
fetch,
|
||||
headers,
|
||||
@@ -473,8 +490,8 @@ describe('ParseGraphQLServer', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
spyOn(console, 'warn').and.callFake(() => {});
|
||||
spyOn(console, 'error').and.callFake(() => {});
|
||||
spyOn(console, 'warn').and.callFake(() => { });
|
||||
spyOn(console, 'error').and.callFake(() => { });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -590,6 +607,96 @@ describe('ParseGraphQLServer', () => {
|
||||
]);
|
||||
};
|
||||
|
||||
describe('Introspection', () => {
|
||||
it('should have public introspection disabled by default without master key', async () => {
|
||||
|
||||
try {
|
||||
await apolloClient.query({
|
||||
query: gql`
|
||||
query Introspection {
|
||||
__schema {
|
||||
types {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
|
||||
fail('should have thrown an error');
|
||||
|
||||
} catch (e) {
|
||||
expect(e.message).toEqual('Response not successful: Received status code 403');
|
||||
expect(e.networkError.result.errors[0].message).toEqual('Introspection is not allowed');
|
||||
}
|
||||
});
|
||||
|
||||
it('should always work with master key', async () => {
|
||||
const introspection =
|
||||
await apolloClient.query({
|
||||
query: gql`
|
||||
query Introspection {
|
||||
__schema {
|
||||
types {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
context: {
|
||||
headers: {
|
||||
'X-Parse-Master-Key': 'test',
|
||||
},
|
||||
}
|
||||
},)
|
||||
expect(introspection.data).toBeDefined();
|
||||
expect(introspection.errors).not.toBeDefined();
|
||||
});
|
||||
|
||||
it('should always work with maintenance key', async () => {
|
||||
const introspection =
|
||||
await apolloClient.query({
|
||||
query: gql`
|
||||
query Introspection {
|
||||
__schema {
|
||||
types {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
context: {
|
||||
headers: {
|
||||
'X-Parse-Maintenance-Key': 'test2',
|
||||
},
|
||||
}
|
||||
},)
|
||||
expect(introspection.data).toBeDefined();
|
||||
expect(introspection.errors).not.toBeDefined();
|
||||
});
|
||||
|
||||
it('should have public introspection enabled if enabled', async () => {
|
||||
|
||||
const parseServer = await reconfigureServer();
|
||||
await createGQLFromParseServer(parseServer, { graphQLPublicIntrospection: true });
|
||||
|
||||
const introspection =
|
||||
await apolloClient.query({
|
||||
query: gql`
|
||||
query Introspection {
|
||||
__schema {
|
||||
types {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
expect(introspection.data).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Default Types', () => {
|
||||
it('should have Object scalar type', async () => {
|
||||
const objectType = (
|
||||
@@ -734,6 +841,11 @@ describe('ParseGraphQLServer', () => {
|
||||
}
|
||||
}
|
||||
`,
|
||||
context: {
|
||||
headers: {
|
||||
'X-Parse-Master-Key': 'test',
|
||||
},
|
||||
}
|
||||
})
|
||||
).data['__schema'].types.map(type => type.name);
|
||||
|
||||
@@ -753,10 +865,6 @@ describe('ParseGraphQLServer', () => {
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await resetGraphQLCache();
|
||||
});
|
||||
|
||||
it('should have Node interface', async () => {
|
||||
const schemaTypes = (
|
||||
await apolloClient.query({
|
||||
@@ -769,6 +877,11 @@ describe('ParseGraphQLServer', () => {
|
||||
}
|
||||
}
|
||||
`,
|
||||
context: {
|
||||
headers: {
|
||||
'X-Parse-Master-Key': 'test',
|
||||
},
|
||||
}
|
||||
})
|
||||
).data['__schema'].types.map(type => type.name);
|
||||
|
||||
@@ -853,7 +966,7 @@ describe('ParseGraphQLServer', () => {
|
||||
});
|
||||
|
||||
it('should have clientMutationId in call function input', async () => {
|
||||
Parse.Cloud.define('hello', () => {});
|
||||
Parse.Cloud.define('hello', () => { });
|
||||
|
||||
const callFunctionInputFields = (
|
||||
await apolloClient.query({
|
||||
@@ -875,7 +988,7 @@ describe('ParseGraphQLServer', () => {
|
||||
});
|
||||
|
||||
it('should have clientMutationId in call function payload', async () => {
|
||||
Parse.Cloud.define('hello', () => {});
|
||||
Parse.Cloud.define('hello', () => { });
|
||||
|
||||
const callFunctionPayloadFields = (
|
||||
await apolloClient.query({
|
||||
@@ -1301,6 +1414,11 @@ describe('ParseGraphQLServer', () => {
|
||||
}
|
||||
}
|
||||
`,
|
||||
context: {
|
||||
headers: {
|
||||
'X-Parse-Master-Key': 'test',
|
||||
},
|
||||
}
|
||||
})
|
||||
).data['__schema'].types.map(type => type.name);
|
||||
|
||||
@@ -1395,7 +1513,7 @@ describe('ParseGraphQLServer', () => {
|
||||
await resetGraphQLCache();
|
||||
});
|
||||
|
||||
it('should only include types in the enabledForClasses list', async () => {
|
||||
it_id('d6a23a2f-ca18-4b15-bc73-3e636f99e6bc')(it)('should only include types in the enabledForClasses list', async () => {
|
||||
const schemaController = await parseServer.config.databaseController.loadSchema();
|
||||
await schemaController.addClassIfNotExists('SuperCar', {
|
||||
foo: { type: 'String' },
|
||||
@@ -1426,7 +1544,7 @@ describe('ParseGraphQLServer', () => {
|
||||
expect(data.userType).toBeNull();
|
||||
expect(data.superCarType).toBeTruthy();
|
||||
});
|
||||
it('should not include types in the disabledForClasses list', async () => {
|
||||
it_id('1db2aceb-d24e-4929-ba43-8dbb5d0395e1')(it)('should not include types in the disabledForClasses list', async () => {
|
||||
const schemaController = await parseServer.config.databaseController.loadSchema();
|
||||
await schemaController.addClassIfNotExists('SuperCar', {
|
||||
foo: { type: 'String' },
|
||||
@@ -1457,7 +1575,7 @@ describe('ParseGraphQLServer', () => {
|
||||
expect(data.superCarType).toBeNull();
|
||||
expect(data.userType).toBeTruthy();
|
||||
});
|
||||
it('should remove query operations when disabled', async () => {
|
||||
it_id('85c2e02f-0239-4819-b66e-392e0125f6c5')(it)('should remove query operations when disabled', async () => {
|
||||
const superCar = new Parse.Object('SuperCar');
|
||||
await superCar.save({ foo: 'bar' });
|
||||
const customer = new Parse.Object('Customer');
|
||||
@@ -1563,7 +1681,7 @@ describe('ParseGraphQLServer', () => {
|
||||
).toBeRejected();
|
||||
});
|
||||
|
||||
it('should remove mutation operations, create, update and delete, when disabled', async () => {
|
||||
it_id('972161a6-8108-4e99-a1a5-71d0267d26c2')(it)('should remove mutation operations, create, update and delete, when disabled', async () => {
|
||||
const superCar1 = new Parse.Object('SuperCar');
|
||||
await superCar1.save({ foo: 'bar' });
|
||||
const customer1 = new Parse.Object('Customer');
|
||||
@@ -1735,7 +1853,7 @@ describe('ParseGraphQLServer', () => {
|
||||
).toBeRejected();
|
||||
});
|
||||
|
||||
it('should only allow the supplied create and update fields for a class', async () => {
|
||||
it_id('4af763b1-ff86-43c7-ba30-060a1c07e730')(it)('should only allow the supplied create and update fields for a class', async () => {
|
||||
const schemaController = await parseServer.config.databaseController.loadSchema();
|
||||
await schemaController.addClassIfNotExists('SuperCar', {
|
||||
engine: { type: 'String' },
|
||||
@@ -1823,7 +1941,7 @@ describe('ParseGraphQLServer', () => {
|
||||
expect(updatedSuperCar).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should handle required fields from the Parse class', async () => {
|
||||
it_id('fc9237e9-3e63-4b55-9c1d-e6269f613a93')(it)('should handle required fields from the Parse class', async () => {
|
||||
const schemaController = await parseServer.config.databaseController.loadSchema();
|
||||
await schemaController.addClassIfNotExists('SuperCar', {
|
||||
engine: { type: 'String', required: true },
|
||||
@@ -1875,7 +1993,7 @@ describe('ParseGraphQLServer', () => {
|
||||
expect(__type2.fields.find(o => o.name === 'doors').type.kind).toEqual('NON_NULL');
|
||||
});
|
||||
|
||||
it('should only allow the supplied output fields for a class', async () => {
|
||||
it_id('83b6895a-7dfd-4e3b-a5ce-acdb1fa39705')(it)('should only allow the supplied output fields for a class', async () => {
|
||||
const schemaController = await parseServer.config.databaseController.loadSchema();
|
||||
|
||||
await schemaController.addClassIfNotExists('SuperCar', {
|
||||
@@ -1992,7 +2110,7 @@ describe('ParseGraphQLServer', () => {
|
||||
expect(getSuperCar.objectId).toBe(superCar.id);
|
||||
});
|
||||
|
||||
it('should only allow the supplied constraint fields for a class', async () => {
|
||||
it_id('67dfcf94-92fb-45a3-a012-3b22c81899ba')(it)('should only allow the supplied constraint fields for a class', async () => {
|
||||
try {
|
||||
const schemaController = await parseServer.config.databaseController.loadSchema();
|
||||
|
||||
@@ -2067,7 +2185,7 @@ describe('ParseGraphQLServer', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should only allow the supplied sort fields for a class', async () => {
|
||||
it_id('a3bdbd5d-8779-42fe-91a1-7a7f90a6177b')(it)('should only allow the supplied sort fields for a class', async () => {
|
||||
const schemaController = await parseServer.config.databaseController.loadSchema();
|
||||
|
||||
await schemaController.addClassIfNotExists('SuperCar', {
|
||||
@@ -2821,7 +2939,8 @@ describe('ParseGraphQLServer', () => {
|
||||
}
|
||||
});
|
||||
it('Id inputs should work either with global id or object id with objectId higher than 19', async () => {
|
||||
await reconfigureServer({ objectIdSize: 20 });
|
||||
const parseServer = await reconfigureServer({ objectIdSize: 20 });
|
||||
await createGQLFromParseServer(parseServer);
|
||||
const obj = new Parse.Object('SomeClass');
|
||||
await obj.save({ name: 'aname', type: 'robot' });
|
||||
const result = await apolloClient.query({
|
||||
@@ -4921,7 +5040,7 @@ describe('ParseGraphQLServer', () => {
|
||||
).toEqual(['someValue1', 'someValue2']);
|
||||
});
|
||||
|
||||
it('should support full text search', async () => {
|
||||
it_id('accc59be-fd13-46c5-a103-ec63f2ad6670')(it)('should support full text search', async () => {
|
||||
try {
|
||||
const obj = new Parse.Object('FullTextSearchTest');
|
||||
obj.set('field1', 'Parse GraphQL Server');
|
||||
@@ -5032,7 +5151,7 @@ describe('ParseGraphQLServer', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should support order, skip and first arguments', async () => {
|
||||
it_id('0fd03d3c-a2c8-4fac-95cc-2391a3032ca2')(it)('should support order, skip and first arguments', async () => {
|
||||
const promises = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const obj = new Parse.Object('SomeClass');
|
||||
@@ -5079,7 +5198,7 @@ describe('ParseGraphQLServer', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('should support pagination', async () => {
|
||||
it_id('588a70c6-2932-4d3b-a838-a74c59d8cffb')(it)('should support pagination', async () => {
|
||||
const numberArray = (first, last) => {
|
||||
const array = [];
|
||||
for (let i = first; i <= last; i++) {
|
||||
@@ -5221,7 +5340,7 @@ describe('ParseGraphQLServer', () => {
|
||||
expect(result.data.someClasses.pageInfo.hasNextPage).toEqual(true);
|
||||
});
|
||||
|
||||
it('should support count', async () => {
|
||||
it_id('4f6a5f20-9642-4cf0-b31d-e739672a9096')(it)('should support count', async () => {
|
||||
await prepareData();
|
||||
|
||||
await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
|
||||
@@ -5324,11 +5443,11 @@ describe('ParseGraphQLServer', () => {
|
||||
expect(result.data.find.count).toEqual(2);
|
||||
});
|
||||
|
||||
it('should respect max limit', async () => {
|
||||
it_id('942b57be-ca8a-4a5b-8104-2adef8743b1a')(it)('should respect max limit', async () => {
|
||||
parseServer = await global.reconfigureServer({
|
||||
maxLimit: 10,
|
||||
});
|
||||
|
||||
await createGQLFromParseServer(parseServer);
|
||||
const promises = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const obj = new Parse.Object('SomeClass');
|
||||
@@ -5365,7 +5484,7 @@ describe('ParseGraphQLServer', () => {
|
||||
expect(result.data.find.count).toEqual(100);
|
||||
});
|
||||
|
||||
it('should support keys argument', async () => {
|
||||
it_id('952634f0-0ad5-4a08-8da2-187c1bd9ee94')(it)('should support keys argument', async () => {
|
||||
await prepareData();
|
||||
|
||||
await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
|
||||
@@ -5771,7 +5890,7 @@ describe('ParseGraphQLServer', () => {
|
||||
).toEqual([object3.id, object1.id, object2.id]);
|
||||
});
|
||||
|
||||
it('should support including relation', async () => {
|
||||
it_id('47a6adf3-1cb4-4d92-b74c-e480363f9cb5')(it)('should support including relation', async () => {
|
||||
await prepareData();
|
||||
|
||||
await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
|
||||
@@ -6676,7 +6795,7 @@ describe('ParseGraphQLServer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should unset fields when null used on update/create', async () => {
|
||||
it_id('f722e98e-1fd7-45c5-ade3-5177e3d542e8')(it)('should unset fields when null used on update/create', async () => {
|
||||
const customerSchema = new Parse.Schema('Customer');
|
||||
customerSchema.addString('aString');
|
||||
customerSchema.addBoolean('aBoolean');
|
||||
@@ -6841,7 +6960,7 @@ describe('ParseGraphQLServer', () => {
|
||||
parseServer = await global.reconfigureServer({
|
||||
publicServerURL: 'http://localhost:13377/parse',
|
||||
});
|
||||
|
||||
await createGQLFromParseServer(parseServer);
|
||||
const body = new FormData();
|
||||
body.append(
|
||||
'operations',
|
||||
@@ -7049,6 +7168,7 @@ describe('ParseGraphQLServer', () => {
|
||||
challengeAdapter,
|
||||
},
|
||||
});
|
||||
await createGQLFromParseServer(parseServer);
|
||||
const clientMutationId = uuidv4();
|
||||
|
||||
const result = await apolloClient.mutate({
|
||||
@@ -7095,6 +7215,7 @@ describe('ParseGraphQLServer', () => {
|
||||
challengeAdapter,
|
||||
},
|
||||
});
|
||||
await createGQLFromParseServer(parseServer);
|
||||
const clientMutationId = uuidv4();
|
||||
const userSchema = new Parse.Schema('_User');
|
||||
userSchema.addString('someField');
|
||||
@@ -7169,7 +7290,7 @@ describe('ParseGraphQLServer', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await createGQLFromParseServer(parseServer);
|
||||
userSchema.addString('someField');
|
||||
userSchema.addPointer('aPointer', '_User');
|
||||
await userSchema.update();
|
||||
@@ -7239,7 +7360,7 @@ describe('ParseGraphQLServer', () => {
|
||||
challengeAdapter,
|
||||
},
|
||||
});
|
||||
|
||||
await createGQLFromParseServer(parseServer);
|
||||
const user = new Parse.User();
|
||||
await user.save({ username: 'username', password: 'password' });
|
||||
|
||||
@@ -7310,6 +7431,7 @@ describe('ParseGraphQLServer', () => {
|
||||
challengeAdapter,
|
||||
},
|
||||
});
|
||||
await createGQLFromParseServer(parseServer);
|
||||
const clientMutationId = uuidv4();
|
||||
const user = new Parse.User();
|
||||
user.setUsername('user1');
|
||||
@@ -7432,15 +7554,16 @@ describe('ParseGraphQLServer', () => {
|
||||
it('should send reset password', async () => {
|
||||
const clientMutationId = uuidv4();
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: () => {},
|
||||
sendVerificationEmail: () => { },
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {},
|
||||
sendMail: () => { },
|
||||
};
|
||||
parseServer = await global.reconfigureServer({
|
||||
appName: 'test',
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: 'http://test.test',
|
||||
});
|
||||
await createGQLFromParseServer(parseServer);
|
||||
const user = new Parse.User();
|
||||
user.setUsername('user1');
|
||||
user.setPassword('user1');
|
||||
@@ -7472,11 +7595,11 @@ describe('ParseGraphQLServer', () => {
|
||||
const clientMutationId = uuidv4();
|
||||
let resetPasswordToken;
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: () => {},
|
||||
sendVerificationEmail: () => { },
|
||||
sendPasswordResetEmail: ({ link }) => {
|
||||
resetPasswordToken = link.split('token=')[1].split('&')[0];
|
||||
},
|
||||
sendMail: () => {},
|
||||
sendMail: () => { },
|
||||
};
|
||||
parseServer = await global.reconfigureServer({
|
||||
appName: 'test',
|
||||
@@ -7488,6 +7611,7 @@ describe('ParseGraphQLServer', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
await createGQLFromParseServer(parseServer);
|
||||
const user = new Parse.User();
|
||||
user.setUsername('user1');
|
||||
user.setPassword('user1');
|
||||
@@ -7541,15 +7665,16 @@ describe('ParseGraphQLServer', () => {
|
||||
it('should send verification email again', async () => {
|
||||
const clientMutationId = uuidv4();
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: () => {},
|
||||
sendVerificationEmail: () => { },
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {},
|
||||
sendMail: () => { },
|
||||
};
|
||||
parseServer = await global.reconfigureServer({
|
||||
appName: 'test',
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: 'http://test.test',
|
||||
});
|
||||
await createGQLFromParseServer(parseServer);
|
||||
const user = new Parse.User();
|
||||
user.setUsername('user1');
|
||||
user.setPassword('user1');
|
||||
@@ -8320,7 +8445,7 @@ describe('ParseGraphQLServer', () => {
|
||||
expect(schema.fields.updatedAt.type).toEqual('Date');
|
||||
});
|
||||
|
||||
it('should support ACL', async () => {
|
||||
it_id('93e748f6-ad9b-4c31-8e1e-c5685e2382fb')(it)('should support ACL', async () => {
|
||||
const someClass = new Parse.Object('SomeClass');
|
||||
await someClass.save();
|
||||
|
||||
@@ -9074,7 +9199,7 @@ describe('ParseGraphQLServer', () => {
|
||||
expect(result2.companies.edges[0].node.objectId).toEqual(company1.id);
|
||||
});
|
||||
|
||||
it('should support relational where query', async () => {
|
||||
it_id('f4312f2c-90bb-4583-b033-02078ae0ce84')(it)('should support relational where query', async () => {
|
||||
const president = new Parse.Object('President');
|
||||
president.set('name', 'James');
|
||||
await president.save();
|
||||
@@ -9306,7 +9431,7 @@ describe('ParseGraphQLServer', () => {
|
||||
parseServer = await global.reconfigureServer({
|
||||
publicServerURL: 'http://localhost:13377/parse',
|
||||
});
|
||||
|
||||
await createGQLFromParseServer(parseServer);
|
||||
const body = new FormData();
|
||||
body.append(
|
||||
'operations',
|
||||
@@ -9339,7 +9464,6 @@ describe('ParseGraphQLServer', () => {
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
|
||||
const result = JSON.parse(await res.text());
|
||||
@@ -9553,6 +9677,7 @@ describe('ParseGraphQLServer', () => {
|
||||
parseServer = await global.reconfigureServer({
|
||||
publicServerURL: 'http://localhost:13377/parse',
|
||||
});
|
||||
await createGQLFromParseServer(parseServer);
|
||||
const schemaController = await parseServer.config.databaseController.loadSchema();
|
||||
await schemaController.addClassIfNotExists('SomeClassWithRequiredFile', {
|
||||
someField: { type: 'File', required: true },
|
||||
@@ -9617,6 +9742,7 @@ describe('ParseGraphQLServer', () => {
|
||||
parseServer = await global.reconfigureServer({
|
||||
publicServerURL: 'http://localhost:13377/parse',
|
||||
});
|
||||
await createGQLFromParseServer(parseServer);
|
||||
const schema = new Parse.Schema('SomeClass');
|
||||
schema.addFile('someFileField');
|
||||
schema.addPointer('somePointerField', 'SomeClass');
|
||||
@@ -9725,7 +9851,7 @@ describe('ParseGraphQLServer', () => {
|
||||
parseServer = await global.reconfigureServer({
|
||||
publicServerURL: 'http://localhost:13377/parse',
|
||||
});
|
||||
|
||||
await createGQLFromParseServer(parseServer);
|
||||
const body = new FormData();
|
||||
body.append(
|
||||
'operations',
|
||||
@@ -10374,7 +10500,7 @@ describe('ParseGraphQLServer', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should support Bytes', async () => {
|
||||
it_id('43303db7-c5a7-4bc0-91c3-57e03fffa225')(it)('should support Bytes', async () => {
|
||||
try {
|
||||
const someFieldValue = 'aGVsbG8gd29ybGQ=';
|
||||
|
||||
@@ -10461,7 +10587,7 @@ describe('ParseGraphQLServer', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should support Geo Points', async () => {
|
||||
it_id('6a253e47-6959-4427-b841-c0c1fa77cf01')(it)('should support Geo Points', async () => {
|
||||
try {
|
||||
const someFieldValue = {
|
||||
__typename: 'GeoPoint',
|
||||
@@ -11008,7 +11134,7 @@ describe('ParseGraphQLServer', () => {
|
||||
});
|
||||
parseGraphQLServer.applyGraphQL(expressApp);
|
||||
await new Promise(resolve => httpServer.listen({ port: 13377 }, resolve));
|
||||
const httpLink = createUploadLink({
|
||||
const httpLink = await createUploadLink({
|
||||
uri: 'http://localhost:13377/graphql',
|
||||
fetch,
|
||||
headers,
|
||||
@@ -11232,7 +11358,7 @@ describe('ParseGraphQLServer', () => {
|
||||
|
||||
parseGraphQLServer.applyGraphQL(expressApp);
|
||||
await new Promise(resolve => httpServer.listen({ port: 13377 }, resolve));
|
||||
const httpLink = createUploadLink({
|
||||
const httpLink = await createUploadLink({
|
||||
uri: 'http://localhost:13377/graphql',
|
||||
fetch,
|
||||
headers,
|
||||
@@ -11427,7 +11553,7 @@ describe('ParseGraphQLServer', () => {
|
||||
|
||||
parseGraphQLServer.applyGraphQL(expressApp);
|
||||
await new Promise(resolve => httpServer.listen({ port: 13377 }, resolve));
|
||||
const httpLink = createUploadLink({
|
||||
const httpLink = await createUploadLink({
|
||||
uri: 'http://localhost:13377/graphql',
|
||||
fetch,
|
||||
headers,
|
||||
|
||||
+13
-14
@@ -4,7 +4,6 @@ const request = require('../lib/request');
|
||||
const triggers = require('../lib/triggers');
|
||||
const HooksController = require('../lib/Controllers/HooksController').default;
|
||||
const express = require('express');
|
||||
const bodyParser = require('body-parser');
|
||||
const auth = require('../lib/Auth');
|
||||
const Config = require('../lib/Config');
|
||||
|
||||
@@ -17,7 +16,7 @@ describe('Hooks', () => {
|
||||
beforeEach(done => {
|
||||
if (!app) {
|
||||
app = express();
|
||||
app.use(bodyParser.json({ type: '*/*' }));
|
||||
app.use(express.json({ type: '*/*' }));
|
||||
server = app.listen(port, undefined, done);
|
||||
} else {
|
||||
done();
|
||||
@@ -54,7 +53,7 @@ describe('Hooks', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should CRUD a function registration', done => {
|
||||
it_id('26c9a13d-3d71-452e-a91c-9a4589be021c')(it)('should CRUD a function registration', done => {
|
||||
// Create
|
||||
Parse.Hooks.createFunction('My-Test-Function', 'http://someurl')
|
||||
.then(response => {
|
||||
@@ -98,7 +97,7 @@ describe('Hooks', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should CRUD a trigger registration', done => {
|
||||
it_id('7a81069e-2ee9-47fb-8e27-1120eda09e99')(it)('should CRUD a trigger registration', done => {
|
||||
// Create
|
||||
Parse.Hooks.createTrigger('MyClass', 'beforeDelete', 'http://someurl')
|
||||
.then(
|
||||
@@ -188,7 +187,7 @@ describe('Hooks', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail trying to create two times the same function', done => {
|
||||
it_id('f7ad092f-81dc-4729-afd1-3b02db2f0948')(it)('should fail trying to create two times the same function', done => {
|
||||
Parse.Hooks.createFunction('my_new_function', 'http://url.com')
|
||||
.then(() => jasmine.timeout())
|
||||
.then(
|
||||
@@ -224,7 +223,7 @@ describe('Hooks', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail trying to create two times the same trigger', done => {
|
||||
it_id('4db8c249-9174-4e8e-b959-55c8ea959a02')(it)('should fail trying to create two times the same trigger', done => {
|
||||
Parse.Hooks.createTrigger('MyClass', 'beforeSave', 'http://url.com')
|
||||
.then(
|
||||
() => {
|
||||
@@ -359,7 +358,7 @@ describe('Hooks', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should create hooks and properly preload them', done => {
|
||||
it_id('96d99414-b739-4e36-b3f4-8135e0be83ea')(it)('should create hooks and properly preload them', done => {
|
||||
const promises = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
promises.push(
|
||||
@@ -410,7 +409,7 @@ describe('Hooks', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should run the function on the test server', done => {
|
||||
it_id('fe7d41eb-e570-4804-ac1f-8b6c407fdafe')(it)('should run the function on the test server', done => {
|
||||
app.post('/SomeFunction', function (req, res) {
|
||||
res.json({ success: 'OK!' });
|
||||
});
|
||||
@@ -439,7 +438,7 @@ describe('Hooks', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should run the function on the test server (error handling)', done => {
|
||||
it_id('63985b4c-a212-4a86-aa0e-eb4600bb485b')(it)('should run the function on the test server (error handling)', done => {
|
||||
app.post('/SomeFunctionError', function (req, res) {
|
||||
res.json({ error: { code: 1337, error: 'hacking that one!' } });
|
||||
});
|
||||
@@ -473,7 +472,7 @@ describe('Hooks', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should provide X-Parse-Webhook-Key when defined', done => {
|
||||
it_id('bacc1754-2a3a-4a7a-8d0e-f80af36da1ef')(it)('should provide X-Parse-Webhook-Key when defined', done => {
|
||||
app.post('/ExpectingKey', function (req, res) {
|
||||
if (req.get('X-Parse-Webhook-Key') === 'hook') {
|
||||
res.json({ success: 'correct key provided' });
|
||||
@@ -506,7 +505,7 @@ describe('Hooks', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should not pass X-Parse-Webhook-Key if not provided', done => {
|
||||
it_id('eeb67946-42c6-4581-89af-2abb4927913e')(it)('should not pass X-Parse-Webhook-Key if not provided', done => {
|
||||
reconfigureServer({ webhookKey: undefined }).then(() => {
|
||||
app.post('/ExpectingKeyAlso', function (req, res) {
|
||||
if (req.get('X-Parse-Webhook-Key') === 'hook') {
|
||||
@@ -545,7 +544,7 @@ describe('Hooks', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should run the beforeSave hook on the test server', done => {
|
||||
it_id('21decb65-4b93-4791-85a3-ab124a9ea3ac')(it)('should run the beforeSave hook on the test server', done => {
|
||||
let triggerCount = 0;
|
||||
app.post('/BeforeSaveSome', function (req, res) {
|
||||
triggerCount++;
|
||||
@@ -576,7 +575,7 @@ describe('Hooks', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('beforeSave hooks should correctly handle responses containing entire object', done => {
|
||||
it_id('52e3152b-5514-4418-9e76-1f394368b8fb')(it)('beforeSave hooks should correctly handle responses containing entire object', done => {
|
||||
app.post('/BeforeSaveSome2', function (req, res) {
|
||||
const object = Parse.Object.fromJSON(req.body.object);
|
||||
object.set('hello', 'world');
|
||||
@@ -600,7 +599,7 @@ describe('Hooks', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should run the afterSave hook on the test server', done => {
|
||||
it_id('d27a7587-abb5-40d5-9805-051ee91de474')(it)('should run the afterSave hook on the test server', done => {
|
||||
let triggerCount = 0;
|
||||
let newObjectId;
|
||||
app.post('/AfterSaveSome', function (req, res) {
|
||||
|
||||
@@ -406,7 +406,7 @@ describe('Installations', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('updating with new channels', done => {
|
||||
it_id('95955e90-04bc-4437-920e-b84bc30dba01')(it)('updating with new channels', done => {
|
||||
const input = {
|
||||
installationId: '12345678-abcd-abcd-abcd-123456789abc',
|
||||
deviceType: 'android',
|
||||
@@ -856,7 +856,7 @@ describe('Installations', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('update is linking two existing objects w/ increment', done => {
|
||||
it_id('22311bc7-3f4f-42c1-a958-57083929e80d')(it)('update is linking two existing objects w/ increment', done => {
|
||||
const installId = '12345678-abcd-abcd-abcd-123456789abc';
|
||||
const t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
|
||||
let input = {
|
||||
@@ -969,7 +969,7 @@ describe('Installations', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('update is linking two existing with installation id w/ op', done => {
|
||||
it_id('f2975078-eab7-4287-a932-288842e3cfb9')(it)('update is linking two existing with installation id w/ op', done => {
|
||||
const installId = '12345678-abcd-abcd-abcd-123456789abc';
|
||||
const t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
|
||||
let input = {
|
||||
@@ -1239,7 +1239,7 @@ describe('Installations', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('can use push with beforeSave', async () => {
|
||||
it_id('e581faea-c1b4-4c64-af8c-52287ce6cd06')(it)('can use push with beforeSave', async () => {
|
||||
const input = {
|
||||
deviceToken: '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306',
|
||||
deviceType: 'ios',
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
'use strict';
|
||||
const http = require('http');
|
||||
const Auth = require('../lib/Auth');
|
||||
const UserController = require('../lib/Controllers/UserController').UserController;
|
||||
const Config = require('../lib/Config');
|
||||
const ParseServer = require('../lib/index').ParseServer;
|
||||
const triggers = require('../lib/triggers');
|
||||
const { resolvingPromise, sleep } = require('../lib/TestUtils');
|
||||
const { resolvingPromise, sleep, getConnectionsCount } = require('../lib/TestUtils');
|
||||
const request = require('../lib/request');
|
||||
const validatorFail = () => {
|
||||
throw 'you are not authorized';
|
||||
};
|
||||
@@ -878,7 +880,7 @@ describe('ParseLiveQuery', function () {
|
||||
await expectAsync(query.subscribe()).toBeRejectedWith(new Error('Invalid session token'));
|
||||
});
|
||||
|
||||
it('handle invalid websocket payload length', async done => {
|
||||
it_id('4ccc9508-ae6a-46ec-932a-9f5e49ab3b9e')(it)('handle invalid websocket payload length', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
@@ -915,7 +917,7 @@ describe('ParseLiveQuery', function () {
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
it('should execute live query update on email validation', async done => {
|
||||
it_id('39a9191f-26dd-4e05-a379-297a67928de8')(it)('should execute live query update on email validation', async done => {
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: () => {},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
@@ -969,7 +971,7 @@ describe('ParseLiveQuery', function () {
|
||||
const userController = new UserController(emailAdapter, 'test', {
|
||||
verifyUserEmails: true,
|
||||
});
|
||||
userController.verifyEmail(foundUser.username, foundUser._email_verify_token);
|
||||
userController.verifyEmail(foundUser._email_verify_token);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1145,7 +1147,7 @@ describe('ParseLiveQuery', function () {
|
||||
await object.save();
|
||||
});
|
||||
|
||||
it('does shutdown liveQuery server', async () => {
|
||||
it_id('2f95d8a9-7675-45ba-a4a6-e45cb7efb1fb')(it)('does shutdown liveQuery server', async () => {
|
||||
await reconfigureServer({ appId: 'test_app_id' });
|
||||
const config = {
|
||||
appId: 'hello_test',
|
||||
@@ -1171,14 +1173,90 @@ describe('ParseLiveQuery', function () {
|
||||
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
|
||||
client.serverURL = 'ws://localhost:1345/1';
|
||||
const query = await new Parse.Query('Yolo').subscribe();
|
||||
let liveQueryConnectionCount = await getConnectionsCount(server.liveQueryServer.server);
|
||||
expect(liveQueryConnectionCount > 0).toBe(true);
|
||||
await Promise.all([
|
||||
server.handleShutdown(),
|
||||
new Promise(resolve => query.on('close', resolve)),
|
||||
]);
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
await sleep(100);
|
||||
expect(server.liveQueryServer.server.address()).toBeNull();
|
||||
expect(server.liveQueryServer.subscriber.isOpen).toBeFalse();
|
||||
await new Promise(resolve => server.server.close(resolve));
|
||||
|
||||
liveQueryConnectionCount = await getConnectionsCount(server.liveQueryServer.server);
|
||||
expect(liveQueryConnectionCount).toBe(0);
|
||||
});
|
||||
|
||||
it_id('45655b74-716f-4fa1-a058-67eb21f3c3db')(it)('does shutdown separate liveQuery server', async () => {
|
||||
await reconfigureServer({ appId: 'test_app_id' });
|
||||
let close = false;
|
||||
const config = {
|
||||
appId: 'hello_test',
|
||||
masterKey: 'world',
|
||||
port: 1345,
|
||||
mountPath: '/1',
|
||||
serverURL: 'http://localhost:1345/1',
|
||||
liveQuery: {
|
||||
classNames: ['Yolo'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
liveQueryServerOptions: {
|
||||
port: 1346,
|
||||
},
|
||||
serverCloseComplete: () => {
|
||||
close = true;
|
||||
},
|
||||
};
|
||||
if (process.env.PARSE_SERVER_TEST_DB === 'postgres') {
|
||||
config.databaseAdapter = new databaseAdapter.constructor({
|
||||
uri: databaseURI,
|
||||
collectionPrefix: 'test_',
|
||||
});
|
||||
config.filesAdapter = defaultConfiguration.filesAdapter;
|
||||
}
|
||||
const parseServer = await ParseServer.startApp(config);
|
||||
expect(parseServer.liveQueryServer).toBeDefined();
|
||||
expect(parseServer.liveQueryServer.server).not.toBe(parseServer.server);
|
||||
|
||||
// Open a connection to the liveQuery server
|
||||
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
|
||||
client.serverURL = 'ws://localhost:1346/1';
|
||||
const query = await new Parse.Query('Yolo').subscribe();
|
||||
|
||||
// Open a connection to the parse server
|
||||
const health = await request({
|
||||
method: 'GET',
|
||||
url: `http://localhost:1345/1/health`,
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'hello_test',
|
||||
'X-Parse-Master-Key': 'world',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
agent: new http.Agent({ keepAlive: true }),
|
||||
}).then(res => res.data);
|
||||
expect(health.status).toBe('ok');
|
||||
|
||||
let parseConnectionCount = await getConnectionsCount(parseServer.server);
|
||||
let liveQueryConnectionCount = await getConnectionsCount(parseServer.liveQueryServer.server);
|
||||
|
||||
expect(parseConnectionCount > 0).toBe(true);
|
||||
expect(liveQueryConnectionCount > 0).toBe(true);
|
||||
await Promise.all([
|
||||
parseServer.handleShutdown(),
|
||||
new Promise(resolve => query.on('close', resolve)),
|
||||
]);
|
||||
expect(close).toBe(true);
|
||||
await sleep(100);
|
||||
expect(parseServer.liveQueryServer.server.address()).toBeNull();
|
||||
expect(parseServer.liveQueryServer.subscriber.isOpen).toBeFalse();
|
||||
|
||||
parseConnectionCount = await getConnectionsCount(parseServer.server);
|
||||
liveQueryConnectionCount = await getConnectionsCount(parseServer.liveQueryServer.server);
|
||||
expect(parseConnectionCount).toBe(0);
|
||||
expect(liveQueryConnectionCount).toBe(0);
|
||||
});
|
||||
|
||||
it('prevent afterSave trigger if not exists', async () => {
|
||||
|
||||
@@ -302,7 +302,7 @@ describe('Parse.Object testing', () => {
|
||||
|
||||
it('invalid key name', function (done) {
|
||||
const item = new Parse.Object('Item');
|
||||
ok(!item.set({ 'foo^bar': 'baz' }), 'Item should not be updated with invalid key.');
|
||||
expect(() => item.set({ 'foo^bar': 'baz' })).toThrow(new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'Invalid key name: foo^bar'));
|
||||
item.save({ 'foo^bar': 'baz' }).then(fail, () => done());
|
||||
});
|
||||
|
||||
@@ -603,7 +603,7 @@ describe('Parse.Object testing', () => {
|
||||
expect(result.get('items')).toEqual(obj.get('items'));
|
||||
});
|
||||
|
||||
it('can query array nested fields', async () => {
|
||||
it_id('44097c6f-d0ca-4dc5-aa8a-3dd2d9ac645a')(it)('can query array nested fields', async () => {
|
||||
const objects = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const obj = new TestObject();
|
||||
|
||||
@@ -62,7 +62,7 @@ describe('Parse.Polygon testing', () => {
|
||||
}, done.fail);
|
||||
});
|
||||
|
||||
it('polygon equalTo (open/closed) path', done => {
|
||||
it_id('3019353b-d5b3-4e53-bcb1-716418328bdd')(it)('polygon equalTo (open/closed) path', done => {
|
||||
const openPoints = [
|
||||
[0, 0],
|
||||
[0, 1],
|
||||
|
||||
@@ -69,8 +69,8 @@ const get = function (url, options) {
|
||||
};
|
||||
|
||||
describe('Parse.Query Aggregate testing', () => {
|
||||
beforeEach(done => {
|
||||
loadTestData().then(done, done);
|
||||
beforeEach(async () => {
|
||||
await loadTestData();
|
||||
});
|
||||
|
||||
it('should only query aggregate with master key', done => {
|
||||
@@ -95,7 +95,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('group by field', done => {
|
||||
it_id('add7050f-65d5-4a13-b526-5bd1ee09c7f1')(it)('group by field', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$group: { _id: '$name' },
|
||||
@@ -115,7 +115,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('group by pipeline operator', async () => {
|
||||
it_id('0ab0d776-e45d-419a-9b35-3d11933b77d1')(it)('group by pipeline operator', async () => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
pipeline: {
|
||||
@@ -133,7 +133,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
expect(resp.results[2].objectId).not.toBe(undefined);
|
||||
});
|
||||
|
||||
it('group by empty object', done => {
|
||||
it_id('b6b42145-7eb4-47aa-ada6-8c1444420e07')(it)('group by empty object', done => {
|
||||
const obj = new TestObject();
|
||||
const pipeline = [
|
||||
{
|
||||
@@ -152,7 +152,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('group by empty string', done => {
|
||||
it_id('0f5f6869-e675-41b9-9ad2-52b201124fb0')(it)('group by empty string', done => {
|
||||
const obj = new TestObject();
|
||||
const pipeline = [
|
||||
{
|
||||
@@ -171,7 +171,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('group by empty array', done => {
|
||||
it_id('b9c4f1b4-47f4-4ff4-88fb-586711f57e4a')(it)('group by empty array', done => {
|
||||
const obj = new TestObject();
|
||||
const pipeline = [
|
||||
{
|
||||
@@ -190,7 +190,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('group by multiple columns ', done => {
|
||||
it_id('bf5ee3e5-986c-4994-9c8d-79310283f602')(it)('group by multiple columns ', done => {
|
||||
const obj1 = new TestObject();
|
||||
const obj2 = new TestObject();
|
||||
const obj3 = new TestObject();
|
||||
@@ -216,7 +216,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('group by date object', done => {
|
||||
it_id('3e652c61-78e1-4541-83ac-51ad1def9874')(it)('group by date object', done => {
|
||||
const obj1 = new TestObject();
|
||||
const obj2 = new TestObject();
|
||||
const obj3 = new TestObject();
|
||||
@@ -246,7 +246,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('group by date object transform', done => {
|
||||
it_id('5d3a0f73-1f49-46f3-9be5-caf1eaefec79')(it)('group by date object transform', done => {
|
||||
const obj1 = new TestObject();
|
||||
const obj2 = new TestObject();
|
||||
const obj3 = new TestObject();
|
||||
@@ -276,7 +276,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('group by number', done => {
|
||||
it_id('1f9b10f7-dc0e-467f-b506-a303b9c36258')(it)('group by number', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$group: { _id: '$score' },
|
||||
@@ -296,7 +296,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('group and multiply transform', done => {
|
||||
it_id('c7695018-03de-49e4-8a72-d4d956f70deb')(it_exclude_dbs(['postgres']))('group and multiply transform', done => {
|
||||
const obj1 = new TestObject({ name: 'item a', quantity: 2, price: 10 });
|
||||
const obj2 = new TestObject({ name: 'item b', quantity: 5, price: 5 });
|
||||
const pipeline = [
|
||||
@@ -319,7 +319,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('project and multiply transform', done => {
|
||||
it_id('2d278175-7594-4b29-bef4-04c778b7a42f')(it_exclude_dbs(['postgres']))('project and multiply transform', done => {
|
||||
const obj1 = new TestObject({ name: 'item a', quantity: 2, price: 10 });
|
||||
const obj2 = new TestObject({ name: 'item b', quantity: 5, price: 5 });
|
||||
const pipeline = [
|
||||
@@ -351,7 +351,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('project without objectId transform', done => {
|
||||
it_id('9c9d9318-3a9e-4c2a-8a09-d3aa52c7505b')(it_exclude_dbs(['postgres']))('project without objectId transform', done => {
|
||||
const obj1 = new TestObject({ name: 'item a', quantity: 2, price: 10 });
|
||||
const obj2 = new TestObject({ name: 'item b', quantity: 5, price: 5 });
|
||||
const pipeline = [
|
||||
@@ -383,7 +383,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('project updatedAt only transform', done => {
|
||||
it_id('f92c82ac-1993-4758-b718-45689dfc4154')(it_exclude_dbs(['postgres']))('project updatedAt only transform', done => {
|
||||
const pipeline = [
|
||||
{
|
||||
$project: { _id: 0, updatedAt: 1 },
|
||||
@@ -401,9 +401,9 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])(
|
||||
'can group by any date field (it does not work if you have dirty data)', // rows in your collection with non date data in the field that is supposed to be a date
|
||||
it_id('99566b1d-778d-4444-9deb-c398108e659d')(it_exclude_dbs(['postgres']))('can group by any date field (it does not work if you have dirty data)',
|
||||
done => {
|
||||
// rows in your collection with non date data in the field that is supposed to be a date
|
||||
const obj1 = new TestObject({ dateField2019: new Date(1990, 11, 1) });
|
||||
const obj2 = new TestObject({ dateField2019: new Date(1990, 5, 1) });
|
||||
const obj3 = new TestObject({ dateField2019: new Date(1990, 11, 1) });
|
||||
@@ -439,6 +439,36 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
}
|
||||
);
|
||||
|
||||
it_id('3723671d-4100-4103-ad9c-60e4c22e20ff')(it_exclude_dbs(['postgres']))('matches expression with $dateSubtract from $$NOW', async () => {
|
||||
const obj1 = new TestObject({ date: new Date(new Date().getTime() - 1 * 24 * 60 * 60 * 1_000) }); // 1 day ago
|
||||
const obj2 = new TestObject({ date: new Date(new Date().getTime() - 2 * 24 * 60 * 60 * 1_000) }); // 3 days ago
|
||||
await Parse.Object.saveAll([obj1, obj2]);
|
||||
|
||||
const pipeline = [
|
||||
{
|
||||
$match: {
|
||||
$expr: {
|
||||
$gte: [
|
||||
'$date',
|
||||
{
|
||||
$dateSubtract: {
|
||||
startDate: '$$NOW',
|
||||
unit: 'day',
|
||||
amount: 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const query = new Parse.Query('TestObject');
|
||||
const results = await query.aggregate(pipeline, { useMasterKey: true });
|
||||
expect(results.length).toBe(1);
|
||||
expect(new Date(results[0].date.iso)).toEqual(obj1.get('date'));
|
||||
});
|
||||
|
||||
it_only_db('postgres')(
|
||||
'can group by any date field (it does not work if you have dirty data)', // rows in your collection with non date data in the field that is supposed to be a date
|
||||
done => {
|
||||
@@ -472,7 +502,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
}
|
||||
);
|
||||
|
||||
it('group by pointer', done => {
|
||||
it_id('bf3c2704-b721-4b1b-92fa-e1b129ae4aff')(it)('group by pointer', done => {
|
||||
const pointer1 = new TestObject();
|
||||
const pointer2 = new TestObject();
|
||||
const obj1 = new TestObject({ pointer: pointer1 });
|
||||
@@ -493,7 +523,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('group sum query', done => {
|
||||
it_id('9ee9e8c0-a590-4af9-97a9-4b8e5080ffae')(it)('group sum query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$group: { _id: null, total: { $sum: '$score' } },
|
||||
@@ -509,7 +539,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('group count query', done => {
|
||||
it_id('39133cd6-5bdf-4917-b672-a9d7a9157b6f')(it)('group count query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$group: { _id: null, total: { $sum: 1 } },
|
||||
@@ -525,7 +555,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('group min query', done => {
|
||||
it_id('48685ff3-066f-4353-82e7-87f39d812ff7')(it)('group min query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$group: { _id: null, minScore: { $min: '$score' } },
|
||||
@@ -541,7 +571,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('group max query', done => {
|
||||
it_id('581efea6-6525-4e10-96d9-76d32c73e7a9')(it)('group max query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$group: { _id: null, maxScore: { $max: '$score' } },
|
||||
@@ -557,7 +587,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('group avg query', done => {
|
||||
it_id('5f880de2-b97f-43d1-89b7-ad903a4be4e2')(it)('group avg query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$group: { _id: null, avgScore: { $avg: '$score' } },
|
||||
@@ -573,7 +603,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('limit query', done => {
|
||||
it_id('58e7a1a0-fae1-4993-b336-7bcbd5b7c786')(it)('limit query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$limit: 2,
|
||||
@@ -587,7 +617,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('sort ascending query', done => {
|
||||
it_id('c892a3d2-8ae8-4b88-bf2b-3c958e1cacd8')(it)('sort ascending query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$sort: { name: 1 },
|
||||
@@ -605,7 +635,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('sort decending query', done => {
|
||||
it_id('79d4bc2e-8b69-42ec-8526-20d17e968ab3')(it)('sort decending query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$sort: { name: -1 },
|
||||
@@ -623,7 +653,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('skip query', done => {
|
||||
it_id('b3d97d48-bd6b-444d-be64-cc1fd4738266')(it)('skip query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$skip: 2,
|
||||
@@ -637,7 +667,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('match comparison date query', done => {
|
||||
it_id('4a7daee3-5ba1-4c8b-b406-1846a73a64c8')(it)('match comparison date query', done => {
|
||||
const today = new Date();
|
||||
const yesterday = new Date();
|
||||
const tomorrow = new Date();
|
||||
@@ -658,7 +688,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should aggregate with Date object (directAccess)', async () => {
|
||||
it_id('d98c8c20-6dac-4d74-8228-85a1ae46a7d0')(it)('should aggregate with Date object (directAccess)', async () => {
|
||||
const rest = require('../lib/rest');
|
||||
const auth = require('../lib/Auth');
|
||||
const TestObject = Parse.Object.extend('TestObject');
|
||||
@@ -675,7 +705,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
expect(resp.results.length).toBe(1);
|
||||
});
|
||||
|
||||
it('match comparison query', done => {
|
||||
it_id('3d73d23a-fce1-4ac0-972a-50f6a550f348')(it)('match comparison query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$match: { score: { $gt: 15 } },
|
||||
@@ -690,7 +720,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('match multiple comparison query', done => {
|
||||
it_id('11772059-6c93-41ac-8dfe-e55b6c97e16f')(it)('match multiple comparison query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$match: { score: { $gt: 5, $lt: 15 } },
|
||||
@@ -707,7 +737,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('match complex comparison query', done => {
|
||||
it_id('ca2efb04-8f73-40ca-a5fc-79d0032bc398')(it)('match complex comparison query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$match: { score: { $gt: 5, $lt: 15 }, views: { $gt: 850, $lt: 1000 } },
|
||||
@@ -723,7 +753,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('match comparison and equality query', done => {
|
||||
it_id('5ef9dcbe-fe54-4db2-b8fb-58c87c6ff072')(it)('match comparison and equality query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$match: { score: { $gt: 5, $lt: 15 }, views: 900 },
|
||||
@@ -739,7 +769,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('match $or query', done => {
|
||||
it_id('c910a6af-58df-46aa-bbf8-da014a04cdcd')(it)('match $or query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$match: {
|
||||
@@ -762,7 +792,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('match objectId query', done => {
|
||||
it_id('0f768dc2-0675-4e45-a763-5ca9c895fa5f')(it)('match objectId query', done => {
|
||||
const obj1 = new TestObject();
|
||||
const obj2 = new TestObject();
|
||||
Parse.Object.saveAll([obj1, obj2])
|
||||
@@ -778,7 +808,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('match field query', done => {
|
||||
it_id('27349e04-0d9d-453f-ad85-1a811631582d')(it)('match field query', done => {
|
||||
const obj1 = new TestObject({ name: 'TestObject1' });
|
||||
const obj2 = new TestObject({ name: 'TestObject2' });
|
||||
Parse.Object.saveAll([obj1, obj2])
|
||||
@@ -794,7 +824,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('match pointer query', done => {
|
||||
it_id('9222e025-d450-4699-8d5b-c5cf9a64fb24')(it)('match pointer query', done => {
|
||||
const pointer1 = new PointerObject();
|
||||
const pointer2 = new PointerObject();
|
||||
const obj1 = new TestObject({ pointer: pointer1 });
|
||||
@@ -817,7 +847,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('match exists query', done => {
|
||||
it_id('3a1e2cdc-52c7-4060-bc90-b06d557d85ce')(it_exclude_dbs(['postgres']))('match exists query', done => {
|
||||
const pipeline = [{ $match: { score: { $exists: true } } }];
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.aggregate(pipeline).then(results => {
|
||||
@@ -826,7 +856,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('match date query - createdAt', done => {
|
||||
it_id('0adea3f4-73f7-4b48-a7dd-c764ceb947ec')(it)('match date query - createdAt', done => {
|
||||
const obj1 = new TestObject();
|
||||
const obj2 = new TestObject();
|
||||
|
||||
@@ -845,7 +875,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('match date query - updatedAt', done => {
|
||||
it_id('cdc0eecb-f547-4881-84cc-c06fb46a636a')(it)('match date query - updatedAt', done => {
|
||||
const obj1 = new TestObject();
|
||||
const obj2 = new TestObject();
|
||||
|
||||
@@ -864,7 +894,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('match date query - empty', done => {
|
||||
it_id('621fe00a-1127-4341-a8e1-fc579b7ed8bd')(it)('match date query - empty', done => {
|
||||
const obj1 = new TestObject();
|
||||
const obj2 = new TestObject();
|
||||
|
||||
@@ -882,7 +912,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('match pointer with operator query', done => {
|
||||
it_id('802ffc99-861b-4b72-90a6-0c666a2e3fd8')(it_exclude_dbs(['postgres']))('match pointer with operator query', done => {
|
||||
const pointer = new PointerObject();
|
||||
|
||||
const obj1 = new TestObject({ pointer });
|
||||
@@ -905,7 +935,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('match null values', async () => {
|
||||
it_id('28090280-7c3e-47f8-8bf6-bebf8566a36c')(it_exclude_dbs(['postgres']))('match null values', async () => {
|
||||
const obj1 = new Parse.Object('MyCollection');
|
||||
obj1.set('language', 'en');
|
||||
obj1.set('otherField', 1);
|
||||
@@ -955,7 +985,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
).toEqual([1, 2, 3, 4]);
|
||||
});
|
||||
|
||||
it('project query', done => {
|
||||
it_id('df63d1f5-7c37-4ed9-8bc5-20d82f29f509')(it)('project query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$project: { name: 1 },
|
||||
@@ -975,7 +1005,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('multiple project query', done => {
|
||||
it_id('69224bbb-8ea0-4ab4-af23-398b6432f668')(it)('multiple project query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$project: { name: 1, score: 1, sender: 1 },
|
||||
@@ -995,7 +1025,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('project pointer query', done => {
|
||||
it_id('97ce4c7c-8d9f-4ffd-9352-394bc9867bab')(it)('project pointer query', done => {
|
||||
const pointer = new PointerObject();
|
||||
const obj = new TestObject({ pointer, name: 'hello' });
|
||||
|
||||
@@ -1018,7 +1048,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('project with group query', done => {
|
||||
it_id('3940aac3-ac49-4279-8083-af9096de636f')(it)('project with group query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$project: { score: 1 },
|
||||
@@ -1074,7 +1104,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('distinct query', done => {
|
||||
it_id('985e7a66-d4f5-4f72-bd54-ee44670e0ab0')(it)('distinct query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: { distinct: 'score' },
|
||||
});
|
||||
@@ -1088,7 +1118,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('distinct query with where', done => {
|
||||
it_id('ef157f86-c456-4a4c-8dac-81910bd0f716')(it)('distinct query with where', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
distinct: 'score',
|
||||
@@ -1105,7 +1135,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('distinct query with where string', done => {
|
||||
it_id('7f5275cc-2c34-42bc-8a09-43378419c326')(it)('distinct query with where string', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
distinct: 'score',
|
||||
@@ -1120,7 +1150,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('distinct nested', done => {
|
||||
it_id('383b7248-e457-4373-8d5c-f9359384347e')(it)('distinct nested', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: { distinct: 'sender.group' },
|
||||
});
|
||||
@@ -1134,7 +1164,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('distinct pointer', done => {
|
||||
it_id('20f14464-adb7-428c-ac7a-5a91a1952a64')(it)('distinct pointer', done => {
|
||||
const pointer1 = new PointerObject();
|
||||
const pointer2 = new PointerObject();
|
||||
const obj1 = new TestObject({ pointer: pointer1 });
|
||||
@@ -1153,7 +1183,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('distinct class does not exist return empty', done => {
|
||||
it_id('91e6cb94-2837-44b7-b057-0c4965057caa')(it)('distinct class does not exist return empty', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: { distinct: 'unknown' },
|
||||
});
|
||||
@@ -1165,7 +1195,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('distinct field does not exist return empty', done => {
|
||||
it_id('bd15daaf-8dc7-458c-81e2-170026f4a8a7')(it)('distinct field does not exist return empty', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: { distinct: 'unknown' },
|
||||
});
|
||||
@@ -1182,7 +1212,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('distinct array', done => {
|
||||
it_id('21988fce-8326-425f-82f0-cd444ca3671b')(it)('distinct array', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: { distinct: 'size' },
|
||||
});
|
||||
@@ -1197,13 +1227,13 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('distinct objectId', async () => {
|
||||
it_id('633fde06-c4af-474b-9841-3ccabc24dd4f')(it)('distinct objectId', async () => {
|
||||
const query = new Parse.Query(TestObject);
|
||||
const results = await query.distinct('objectId');
|
||||
expect(results.length).toBe(4);
|
||||
});
|
||||
|
||||
it('distinct createdAt', async () => {
|
||||
it_id('8f9706f4-2703-42f1-b524-f2f7e72bbfe7')(it)('distinct createdAt', async () => {
|
||||
const object1 = new TestObject({ createdAt_test: true });
|
||||
await object1.save();
|
||||
const object2 = new TestObject({ createdAt_test: true });
|
||||
@@ -1214,7 +1244,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
expect(results.length).toBe(2);
|
||||
});
|
||||
|
||||
it('distinct updatedAt', async () => {
|
||||
it_id('3562e600-8ce5-4d6d-96df-8ff969e81421')(it)('distinct updatedAt', async () => {
|
||||
const object1 = new TestObject({ updatedAt_test: true });
|
||||
await object1.save();
|
||||
const object2 = new TestObject();
|
||||
@@ -1227,7 +1257,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
expect(results.length).toBe(2);
|
||||
});
|
||||
|
||||
it('distinct null field', done => {
|
||||
it_id('5012cfb1-b0aa-429d-a94f-d32d8aa0b7f9')(it)('distinct null field', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: { distinct: 'distinctField' },
|
||||
});
|
||||
@@ -1256,7 +1286,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('does not return sensitive hidden properties', done => {
|
||||
it_id('d9c19419-e99d-4d9f-b7f3-418e49ee47dd')(it)('does not return sensitive hidden properties', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$match: {
|
||||
@@ -1305,7 +1335,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('aggregate allow multiple of same stage', async done => {
|
||||
it_id('0a23e791-e9b5-457a-9bf9-9c5ecf406f42')(it_exclude_dbs(['postgres']))('aggregate allow multiple of same stage', async done => {
|
||||
await reconfigureServer({ silent: false });
|
||||
const pointer1 = new TestObject({ value: 1 });
|
||||
const pointer2 = new TestObject({ value: 2 });
|
||||
@@ -1500,4 +1530,24 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
expect(results.length).toEqual(3);
|
||||
await database.adapter.deleteAllClasses(false);
|
||||
});
|
||||
|
||||
it_only_db('mongo')('aggregate handle mongodb errors', async () => {
|
||||
const pipeline = [
|
||||
{
|
||||
$search: {
|
||||
index: "default",
|
||||
text: {
|
||||
path: ["name"],
|
||||
query: 'foo',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
try {
|
||||
await new Parse.Query(TestObject).aggregate(pipeline);
|
||||
fail();
|
||||
} catch (e) {
|
||||
expect(e.code).toBe(Parse.Error.INVALID_QUERY);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,10 +23,7 @@ const profileLevel = 2;
|
||||
describe_only_db('mongo')('Parse.Query with comment testing', () => {
|
||||
beforeAll(async () => {
|
||||
config = Config.get('test');
|
||||
client = await MongoClient.connect(databaseURI, {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true,
|
||||
});
|
||||
client = await MongoClient.connect(databaseURI);
|
||||
database = client.db('parseServerMongoAdapterTestDatabase');
|
||||
let profiler = await database.command({ profile: 0 });
|
||||
expect(profiler.was).toEqual(0);
|
||||
|
||||
@@ -3,11 +3,8 @@
|
||||
const Config = require('../lib/Config');
|
||||
const Parse = require('parse/node');
|
||||
const request = require('../lib/request');
|
||||
let databaseAdapter;
|
||||
|
||||
const fullTextHelper = async () => {
|
||||
const config = Config.get('test');
|
||||
databaseAdapter = config.database.adapter;
|
||||
const subjects = [
|
||||
'coffee',
|
||||
'Coffee Shopping',
|
||||
@@ -18,19 +15,13 @@ const fullTextHelper = async () => {
|
||||
'coffee and cream',
|
||||
'Cafe con Leche',
|
||||
];
|
||||
await reconfigureServer({
|
||||
appId: 'test',
|
||||
restAPIKey: 'test',
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
databaseAdapter,
|
||||
});
|
||||
await Parse.Object.saveAll(
|
||||
subjects.map(subject => new Parse.Object('TestObject').set({ subject, comment: subject }))
|
||||
);
|
||||
};
|
||||
|
||||
describe('Parse.Query Full Text Search testing', () => {
|
||||
it('fullTextSearch: $search', async () => {
|
||||
it_id('77ba6779-6584-4e09-8e7e-31f89e741d6a')(it)('fullTextSearch: $search', async () => {
|
||||
await fullTextHelper();
|
||||
const query = new Parse.Query('TestObject');
|
||||
query.fullText('subject', 'coffee');
|
||||
@@ -38,7 +29,7 @@ describe('Parse.Query Full Text Search testing', () => {
|
||||
expect(results.length).toBe(3);
|
||||
});
|
||||
|
||||
it('fullTextSearch: $search, sort', async () => {
|
||||
it_id('d1992ea6-6d92-4bfa-a487-2a49fbcf8f0d')(it)('fullTextSearch: $search, sort', async () => {
|
||||
await fullTextHelper();
|
||||
const query = new Parse.Query('TestObject');
|
||||
query.fullText('subject', 'coffee');
|
||||
@@ -51,7 +42,7 @@ describe('Parse.Query Full Text Search testing', () => {
|
||||
expect(results[2].get('score'));
|
||||
});
|
||||
|
||||
it('fulltext descending by $score', async () => {
|
||||
it_id('07172595-50de-4be2-984a-d3136bebb22e')(it)('fulltext descending by $score', async () => {
|
||||
await fullTextHelper();
|
||||
const query = new Parse.Query('TestObject');
|
||||
query.fullText('subject', 'coffee');
|
||||
@@ -68,7 +59,7 @@ describe('Parse.Query Full Text Search testing', () => {
|
||||
expect(second.get('score') >= third.get('score')).toBeTrue();
|
||||
});
|
||||
|
||||
it('fullTextSearch: $language', async () => {
|
||||
it_id('8e821973-3fae-4e7c-8152-766228a18cdd')(it)('fullTextSearch: $language', async () => {
|
||||
await fullTextHelper();
|
||||
const query = new Parse.Query('TestObject');
|
||||
query.fullText('subject', 'leche', { language: 'spanish' });
|
||||
@@ -76,7 +67,7 @@ describe('Parse.Query Full Text Search testing', () => {
|
||||
expect(resp.length).toBe(2);
|
||||
});
|
||||
|
||||
it('fullTextSearch: $diacriticSensitive', async () => {
|
||||
it_id('7d3da216-9582-40ee-a2fe-8316feaf5c0c')(it)('fullTextSearch: $diacriticSensitive', async () => {
|
||||
await fullTextHelper();
|
||||
const query = new Parse.Query('TestObject');
|
||||
query.fullText('subject', 'CAFÉ', { diacriticSensitive: true });
|
||||
@@ -84,7 +75,7 @@ describe('Parse.Query Full Text Search testing', () => {
|
||||
expect(resp.length).toBe(1);
|
||||
});
|
||||
|
||||
it('fullTextSearch: $search, invalid input', async () => {
|
||||
it_id('dade10c8-2b9c-4f43-bb3f-a13bbd82ac22')(it)('fullTextSearch: $search, invalid input', async () => {
|
||||
await fullTextHelper();
|
||||
const invalidQuery = async () => {
|
||||
const where = {
|
||||
@@ -101,7 +92,7 @@ describe('Parse.Query Full Text Search testing', () => {
|
||||
body: { where, _method: 'GET' },
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
@@ -114,7 +105,7 @@ describe('Parse.Query Full Text Search testing', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('fullTextSearch: $language, invalid input', async () => {
|
||||
it_id('ff7c6b1c-4712-4847-bb76-f4e1f641f7b5')(it)('fullTextSearch: $language, invalid input', async () => {
|
||||
await fullTextHelper();
|
||||
const query = new Parse.Query('TestObject');
|
||||
query.fullText('subject', 'leche', { language: true });
|
||||
@@ -123,7 +114,7 @@ describe('Parse.Query Full Text Search testing', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('fullTextSearch: $caseSensitive, invalid input', async () => {
|
||||
it_id('de262dbc-ec75-4ec6-9217-fbb90146c272')(it)('fullTextSearch: $caseSensitive, invalid input', async () => {
|
||||
await fullTextHelper();
|
||||
const query = new Parse.Query('TestObject');
|
||||
query.fullText('subject', 'leche', { caseSensitive: 'string' });
|
||||
@@ -132,7 +123,7 @@ describe('Parse.Query Full Text Search testing', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('fullTextSearch: $diacriticSensitive, invalid input', async () => {
|
||||
it_id('b7b7b3a9-8d6c-4f98-a0ff-0113593d06d4')(it)('fullTextSearch: $diacriticSensitive, invalid input', async () => {
|
||||
await fullTextHelper();
|
||||
const query = new Parse.Query('TestObject');
|
||||
query.fullText('subject', 'leche', { diacriticSensitive: 'string' });
|
||||
@@ -189,7 +180,7 @@ describe_only_db('mongo')('[mongodb] Parse.Query Full Text Search testing', () =
|
||||
url: 'http://localhost:8378/1/schemas/TestObject',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
@@ -220,7 +211,7 @@ describe_only_db('mongo')('[mongodb] Parse.Query Full Text Search testing', () =
|
||||
body: { where, _method: 'GET' },
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
@@ -288,7 +279,7 @@ describe_only_db('postgres')('[postgres] Parse.Query Full Text Search testing',
|
||||
body: { where, _method: 'GET' },
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
@@ -322,7 +313,7 @@ describe_only_db('postgres')('[postgres] Parse.Query Full Text Search testing',
|
||||
body: { where, _method: 'GET' },
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
+26
-201
@@ -27,7 +27,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
await TestUtils.destroyAllDataPermanently(false);
|
||||
});
|
||||
|
||||
it_only_mongodb_version('<5.1>=6')('query find with hint string', async () => {
|
||||
it_only_mongodb_version('<5.1 || >=6 <8')('query find with hint string', async () => {
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
|
||||
@@ -39,18 +39,19 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
expect(explain.queryPlanner.winningPlan.inputStage.indexName).toBe('_id_');
|
||||
});
|
||||
|
||||
it_only_mongodb_version('>=5.1<6')('query find with hint string', async () => {
|
||||
it_only_mongodb_version('>=8')('query find with hint string', async () => {
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
|
||||
const collection = await config.database.adapter._adaptiveCollection('TestObject');
|
||||
const explain = await collection._rawFind({ _id: object.id }, { hint: '_id_', explain: true });
|
||||
expect(explain.queryPlanner.winningPlan.queryPlan.stage).toBe('FETCH');
|
||||
expect(explain.queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('IXSCAN');
|
||||
expect(explain.queryPlanner.winningPlan.queryPlan.inputStage.indexName).toBe('_id_');
|
||||
let explain = await collection._rawFind({ _id: object.id }, { explain: true });
|
||||
expect(explain.queryPlanner.winningPlan.stage).toBe('EXPRESS_IXSCAN');
|
||||
explain = await collection._rawFind({ _id: object.id }, { hint: '_id_', explain: true });
|
||||
expect(explain.queryPlanner.winningPlan.stage).toBe('FETCH');
|
||||
expect(explain.queryPlanner.winningPlan.inputStage.indexName).toBe('_id_');
|
||||
});
|
||||
|
||||
it_only_mongodb_version('<5.1>=6')('query find with hint object', async () => {
|
||||
it_only_mongodb_version('<5.1 || >=6 <8')('query find with hint object', async () => {
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
|
||||
@@ -64,21 +65,21 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_only_mongodb_version('>=5.1<6')('query find with hint object', async () => {
|
||||
it_only_mongodb_version('>=8')('query find with hint object', async () => {
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
|
||||
const collection = await config.database.adapter._adaptiveCollection('TestObject');
|
||||
const explain = await collection._rawFind(
|
||||
{ _id: object.id },
|
||||
{ hint: { _id: 1 }, explain: true }
|
||||
);
|
||||
expect(explain.queryPlanner.winningPlan.queryPlan.stage).toBe('FETCH');
|
||||
expect(explain.queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('IXSCAN');
|
||||
expect(explain.queryPlanner.winningPlan.queryPlan.inputStage.keyPattern).toEqual({ _id: 1 });
|
||||
let explain = await collection._rawFind({ _id: object.id }, { explain: true });
|
||||
expect(explain.queryPlanner.winningPlan.stage).toBe('EXPRESS_IXSCAN');
|
||||
explain = await collection._rawFind({ _id: object.id }, { hint: { _id: 1 }, explain: true });
|
||||
expect(explain.queryPlanner.winningPlan.stage).toBe('FETCH');
|
||||
expect(explain.queryPlanner.winningPlan.inputStage.keyPattern).toEqual({
|
||||
_id: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it_only_mongodb_version('<4.4')('query aggregate with hint string', async () => {
|
||||
it_only_mongodb_version('<7')('query aggregate with hint string', async () => {
|
||||
const object = new TestObject({ foo: 'bar' });
|
||||
await object.save();
|
||||
|
||||
@@ -86,29 +87,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
let result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
|
||||
explain: true,
|
||||
});
|
||||
|
||||
let { queryPlanner } = result[0].stages[0].$cursor;
|
||||
expect(queryPlanner.winningPlan.stage).toBe('COLLSCAN');
|
||||
|
||||
result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
|
||||
hint: '_id_',
|
||||
explain: true,
|
||||
});
|
||||
|
||||
queryPlanner = result[0].stages[0].$cursor.queryPlanner;
|
||||
expect(queryPlanner.winningPlan.stage).toBe('FETCH');
|
||||
expect(queryPlanner.winningPlan.inputStage.indexName).toBe('_id_');
|
||||
});
|
||||
|
||||
it_only_mongodb_version('>=4.4<5.1')('query aggregate with hint string', async () => {
|
||||
const object = new TestObject({ foo: 'bar' });
|
||||
await object.save();
|
||||
|
||||
const collection = await config.database.adapter._adaptiveCollection('TestObject');
|
||||
let result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
|
||||
explain: true,
|
||||
});
|
||||
let { queryPlanner } = result[0].stages[0].$cursor;
|
||||
let queryPlanner = result[0].stages[0].$cursor.queryPlanner;
|
||||
expect(queryPlanner.winningPlan.stage).toBe('PROJECTION_SIMPLE');
|
||||
expect(queryPlanner.winningPlan.inputStage.stage).toBe('COLLSCAN');
|
||||
expect(queryPlanner.winningPlan.inputStage.inputStage).toBeUndefined();
|
||||
@@ -124,31 +103,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
expect(queryPlanner.winningPlan.inputStage.inputStage.indexName).toBe('_id_');
|
||||
});
|
||||
|
||||
it_only_mongodb_version('>=5.1<5.2')('query aggregate with hint string', async () => {
|
||||
const object = new TestObject({ foo: 'bar' });
|
||||
await object.save();
|
||||
|
||||
const collection = await config.database.adapter._adaptiveCollection('TestObject');
|
||||
let result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
|
||||
explain: true,
|
||||
});
|
||||
let { queryPlanner } = result[0].stages[0].$cursor;
|
||||
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('PROJECTION_SIMPLE');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('COLLSCAN');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage).toBeUndefined();
|
||||
|
||||
result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
|
||||
hint: '_id_',
|
||||
explain: true,
|
||||
});
|
||||
queryPlanner = result[0].stages[0].$cursor.queryPlanner;
|
||||
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('PROJECTION_SIMPLE');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('FETCH');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.stage).toBe('IXSCAN');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.indexName).toBe('_id_');
|
||||
});
|
||||
|
||||
it_only_mongodb_version('>=5.2')('query aggregate with hint string', async () => {
|
||||
it_only_mongodb_version('>=7')('query aggregate with hint string', async () => {
|
||||
const object = new TestObject({ foo: 'bar' });
|
||||
await object.save();
|
||||
|
||||
@@ -172,7 +127,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.indexName).toBe('_id_');
|
||||
});
|
||||
|
||||
it_only_mongodb_version('<4.4')('query aggregate with hint object', async () => {
|
||||
it_only_mongodb_version('<7')('query aggregate with hint object', async () => {
|
||||
const object = new TestObject({ foo: 'bar' });
|
||||
await object.save();
|
||||
|
||||
@@ -180,27 +135,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
let result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
|
||||
explain: true,
|
||||
});
|
||||
let { queryPlanner } = result[0].stages[0].$cursor;
|
||||
expect(queryPlanner.winningPlan.stage).toBe('COLLSCAN');
|
||||
|
||||
result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
|
||||
hint: { _id: 1 },
|
||||
explain: true,
|
||||
});
|
||||
queryPlanner = result[0].stages[0].$cursor.queryPlanner;
|
||||
expect(queryPlanner.winningPlan.stage).toBe('FETCH');
|
||||
expect(queryPlanner.winningPlan.inputStage.keyPattern).toEqual({ _id: 1 });
|
||||
});
|
||||
|
||||
it_only_mongodb_version('>=4.4<5.1')('query aggregate with hint object', async () => {
|
||||
const object = new TestObject({ foo: 'bar' });
|
||||
await object.save();
|
||||
|
||||
const collection = await config.database.adapter._adaptiveCollection('TestObject');
|
||||
let result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
|
||||
explain: true,
|
||||
});
|
||||
let { queryPlanner } = result[0].stages[0].$cursor;
|
||||
let queryPlanner = result[0].stages[0].$cursor.queryPlanner;
|
||||
expect(queryPlanner.winningPlan.stage).toBe('PROJECTION_SIMPLE');
|
||||
expect(queryPlanner.winningPlan.inputStage.stage).toBe('COLLSCAN');
|
||||
expect(queryPlanner.winningPlan.inputStage.inputStage).toBeUndefined();
|
||||
@@ -217,32 +152,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
expect(queryPlanner.winningPlan.inputStage.inputStage.keyPattern).toEqual({ _id: 1 });
|
||||
});
|
||||
|
||||
it_only_mongodb_version('>=5.1<5.2')('query aggregate with hint object', async () => {
|
||||
const object = new TestObject({ foo: 'bar' });
|
||||
await object.save();
|
||||
|
||||
const collection = await config.database.adapter._adaptiveCollection('TestObject');
|
||||
let result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
|
||||
explain: true,
|
||||
});
|
||||
let { queryPlanner } = result[0].stages[0].$cursor;
|
||||
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('PROJECTION_SIMPLE');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('COLLSCAN');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage).toBeUndefined();
|
||||
|
||||
result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
|
||||
hint: { _id: 1 },
|
||||
explain: true,
|
||||
});
|
||||
queryPlanner = result[0].stages[0].$cursor.queryPlanner;
|
||||
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('PROJECTION_SIMPLE');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('FETCH');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.stage).toBe('IXSCAN');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.indexName).toBe('_id_');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.keyPattern).toEqual({ _id: 1 });
|
||||
});
|
||||
|
||||
it_only_mongodb_version('>=5.2')('query aggregate with hint object', async () => {
|
||||
it_only_mongodb_version('>=7')('query aggregate with hint object', async () => {
|
||||
const object = new TestObject({ foo: 'bar' });
|
||||
await object.save();
|
||||
|
||||
@@ -267,7 +177,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.keyPattern).toEqual({ _id: 1 });
|
||||
});
|
||||
|
||||
it_only_mongodb_version('<5.1>=6')('query find with hint (rest)', async () => {
|
||||
it_only_mongodb_version('<5.1 || >=6')('query find with hint (rest)', async () => {
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
let options = Object.assign({}, masterKeyOptions, {
|
||||
@@ -292,32 +202,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
expect(explain.queryPlanner.winningPlan.inputStage.inputStage.indexName).toBe('_id_');
|
||||
});
|
||||
|
||||
it_only_mongodb_version('>=5.1<6')('query find with hint (rest)', async () => {
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
let options = Object.assign({}, masterKeyOptions, {
|
||||
url: Parse.serverURL + '/classes/TestObject',
|
||||
qs: {
|
||||
explain: true,
|
||||
},
|
||||
});
|
||||
let response = await request(options);
|
||||
let explain = response.data.results;
|
||||
expect(explain.queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('COLLSCAN');
|
||||
|
||||
options = Object.assign({}, masterKeyOptions, {
|
||||
url: Parse.serverURL + '/classes/TestObject',
|
||||
qs: {
|
||||
explain: true,
|
||||
hint: '_id_',
|
||||
},
|
||||
});
|
||||
response = await request(options);
|
||||
explain = response.data.results;
|
||||
expect(explain.queryPlanner.winningPlan.queryPlan.inputStage.inputStage.indexName).toBe('_id_');
|
||||
});
|
||||
|
||||
it_only_mongodb_version('<4.4')('query aggregate with hint (rest)', async () => {
|
||||
it_only_mongodb_version('<7')('query aggregate with hint (rest)', async () => {
|
||||
const object = new TestObject({ foo: 'bar' });
|
||||
await object.save();
|
||||
let options = Object.assign({}, masterKeyOptions, {
|
||||
@@ -328,34 +213,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
},
|
||||
});
|
||||
let response = await request(options);
|
||||
let { queryPlanner } = response.data.results[0].stages[0].$cursor;
|
||||
expect(queryPlanner.winningPlan.stage).toBe('COLLSCAN');
|
||||
|
||||
options = Object.assign({}, masterKeyOptions, {
|
||||
url: Parse.serverURL + '/aggregate/TestObject',
|
||||
qs: {
|
||||
explain: true,
|
||||
hint: '_id_',
|
||||
$group: JSON.stringify({ _id: '$foo' }),
|
||||
},
|
||||
});
|
||||
response = await request(options);
|
||||
queryPlanner = response.data.results[0].stages[0].$cursor.queryPlanner;
|
||||
expect(queryPlanner.winningPlan.inputStage.keyPattern).toEqual({ _id: 1 });
|
||||
});
|
||||
|
||||
it_only_mongodb_version('>=4.4<5.1')('query aggregate with hint (rest)', async () => {
|
||||
const object = new TestObject({ foo: 'bar' });
|
||||
await object.save();
|
||||
let options = Object.assign({}, masterKeyOptions, {
|
||||
url: Parse.serverURL + '/aggregate/TestObject',
|
||||
qs: {
|
||||
explain: true,
|
||||
$group: JSON.stringify({ _id: '$foo' }),
|
||||
},
|
||||
});
|
||||
let response = await request(options);
|
||||
let { queryPlanner } = response.data.results[0].stages[0].$cursor;
|
||||
let queryPlanner = response.data.results[0].stages[0].$cursor.queryPlanner;
|
||||
expect(queryPlanner.winningPlan.stage).toBe('PROJECTION_SIMPLE');
|
||||
expect(queryPlanner.winningPlan.inputStage.stage).toBe('COLLSCAN');
|
||||
expect(queryPlanner.winningPlan.inputStage.inputStage).toBeUndefined();
|
||||
@@ -377,40 +235,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
expect(queryPlanner.winningPlan.inputStage.inputStage.keyPattern).toEqual({ _id: 1 });
|
||||
});
|
||||
|
||||
it_only_mongodb_version('>=5.1<5.2')('query aggregate with hint (rest)', async () => {
|
||||
const object = new TestObject({ foo: 'bar' });
|
||||
await object.save();
|
||||
let options = Object.assign({}, masterKeyOptions, {
|
||||
url: Parse.serverURL + '/aggregate/TestObject',
|
||||
qs: {
|
||||
explain: true,
|
||||
$group: JSON.stringify({ _id: '$foo' }),
|
||||
},
|
||||
});
|
||||
let response = await request(options);
|
||||
let { queryPlanner } = response.data.results[0].stages[0].$cursor;
|
||||
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('PROJECTION_SIMPLE');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('COLLSCAN');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage).toBeUndefined();
|
||||
|
||||
options = Object.assign({}, masterKeyOptions, {
|
||||
url: Parse.serverURL + '/aggregate/TestObject',
|
||||
qs: {
|
||||
explain: true,
|
||||
hint: '_id_',
|
||||
$group: JSON.stringify({ _id: '$foo' }),
|
||||
},
|
||||
});
|
||||
response = await request(options);
|
||||
queryPlanner = response.data.results[0].stages[0].$cursor.queryPlanner;
|
||||
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('PROJECTION_SIMPLE');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('FETCH');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.stage).toBe('IXSCAN');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.indexName).toBe('_id_');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.keyPattern).toEqual({ _id: 1 });
|
||||
});
|
||||
|
||||
it_only_mongodb_version('>=5.2')('query aggregate with hint (rest)', async () => {
|
||||
it_only_mongodb_version('>=7')('query aggregate with hint (rest)', async () => {
|
||||
const object = new TestObject({ foo: 'bar' });
|
||||
await object.save();
|
||||
let options = Object.assign({}, masterKeyOptions, {
|
||||
|
||||
+110
-11
@@ -6,6 +6,8 @@
|
||||
|
||||
const Parse = require('parse/node');
|
||||
const request = require('../lib/request');
|
||||
const ParseServerRESTController = require('../lib/ParseServerRESTController').ParseServerRESTController;
|
||||
const ParseServer = require('../lib/ParseServer').default;
|
||||
|
||||
const masterKeyHeaders = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
@@ -590,7 +592,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('containsAll object array queries', function (done) {
|
||||
it_id('25bb35a6-e953-4d6d-a31c-66324d5ae076')(it)('containsAll object array queries', function (done) {
|
||||
const MessageSet = Parse.Object.extend({ className: 'MessageSet' });
|
||||
|
||||
const messageList = [];
|
||||
@@ -705,7 +707,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('containsAllStartingWith values must be all of type starting with regex', done => {
|
||||
it_id('3ea6ae04-bcc2-453d-8817-4c64d059c2f6')(it)('containsAllStartingWith values must be all of type starting with regex', done => {
|
||||
const object = new Parse.Object('Object');
|
||||
object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']);
|
||||
|
||||
@@ -884,7 +886,7 @@ describe('Parse.Query testing', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('containedBy pointer array', done => {
|
||||
it_id('01a15195-dde2-4368-b996-d746a4ede3a1')(it)('containedBy pointer array', done => {
|
||||
const objects = Array.from(Array(10).keys()).map(idx => {
|
||||
const obj = new Parse.Object('Object');
|
||||
obj.set('key', idx);
|
||||
@@ -1668,7 +1670,7 @@ describe('Parse.Query testing', () => {
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('can order on an object number field', function (done) {
|
||||
it_id('65c8238d-cf02-49d0-a919-8a17f5a58280')(it)('can order on an object number field', function (done) {
|
||||
const testSet = [
|
||||
{ sortField: { value: 10 } },
|
||||
{ sortField: { value: 1 } },
|
||||
@@ -1689,7 +1691,7 @@ describe('Parse.Query testing', () => {
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('can order on an object number field (level 2)', function (done) {
|
||||
it_id('d8f0bead-b931-4d66-8b0c-28c5705e463c')(it)('can order on an object number field (level 2)', function (done) {
|
||||
const testSet = [
|
||||
{ sortField: { value: { field: 10 } } },
|
||||
{ sortField: { value: { field: 1 } } },
|
||||
@@ -2111,7 +2113,7 @@ describe('Parse.Query testing', () => {
|
||||
.then(done);
|
||||
});
|
||||
|
||||
it('Use a regex that requires all modifiers', function (done) {
|
||||
it_id('823852f6-1de5-45ba-a2b9-ed952fcc6012')(it)('Use a regex that requires all modifiers', function (done) {
|
||||
const thing = new TestObject();
|
||||
thing.set('myString', 'PArSe\nCom');
|
||||
Parse.Object.saveAll([thing]).then(function () {
|
||||
@@ -3786,7 +3788,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('notEqual with array of pointers', done => {
|
||||
it_id('56b09b92-c756-4bae-8c32-1c32b5b4c397')(it)('notEqual with array of pointers', done => {
|
||||
const children = [];
|
||||
const parents = [];
|
||||
const promises = [];
|
||||
@@ -4003,7 +4005,7 @@ describe('Parse.Query testing', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should properly interpret a query v2', done => {
|
||||
it_id('7079f0ef-47b3-4a1e-aac0-32654dadaa27')(it)('should properly interpret a query v2', done => {
|
||||
const user = new Parse.User();
|
||||
user.set('username', 'foo');
|
||||
user.set('password', 'bar');
|
||||
@@ -4082,7 +4084,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should find objects with array of pointers', done => {
|
||||
it_id('d95818c0-9e3c-41e6-be20-e7bafb59eefb')(it)('should find objects with array of pointers', done => {
|
||||
const objects = [];
|
||||
while (objects.length != 5) {
|
||||
const object = new Parse.Object('ContainedObject');
|
||||
@@ -5013,7 +5015,7 @@ describe('Parse.Query testing', () => {
|
||||
equal(results[0].get('name'), group2.get('name'));
|
||||
});
|
||||
|
||||
it('withJSON supports geoWithin.centerSphere', done => {
|
||||
it_id('8886b994-fbb8-487d-a863-43bbd2b24b73')(it)('withJSON supports geoWithin.centerSphere', done => {
|
||||
const inbound = new Parse.GeoPoint(1.5, 1.5);
|
||||
const onbound = new Parse.GeoPoint(10, 10);
|
||||
const outbound = new Parse.GeoPoint(20, 20);
|
||||
@@ -5114,7 +5116,7 @@ describe('Parse.Query testing', () => {
|
||||
.catch(() => done());
|
||||
});
|
||||
|
||||
it('can add new config to existing config', async () => {
|
||||
it_id('02d4e7e6-859a-4ab6-878d-135ccc77040e')(it)('can add new config to existing config', async () => {
|
||||
await request({
|
||||
method: 'PUT',
|
||||
url: 'http://localhost:8378/1/config',
|
||||
@@ -5275,4 +5277,101 @@ describe('Parse.Query testing', () => {
|
||||
// Validate
|
||||
expect(result.executionStats).not.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should query with distinct within eachBatch and direct access enabled', async () => {
|
||||
await reconfigureServer({
|
||||
directAccess: true,
|
||||
});
|
||||
|
||||
Parse.CoreManager.setRESTController(
|
||||
ParseServerRESTController(Parse.applicationId, ParseServer.promiseRouter({ appId: Parse.applicationId }))
|
||||
);
|
||||
|
||||
const user = new Parse.User();
|
||||
user.set('username', 'foo');
|
||||
user.set('password', 'bar');
|
||||
await user.save();
|
||||
|
||||
const score = new Parse.Object('Score');
|
||||
score.set('player', user);
|
||||
score.set('score', 1);
|
||||
await score.save();
|
||||
|
||||
await new Parse.Query('_User')
|
||||
.equalTo('objectId', user.id)
|
||||
.eachBatch(async ([user]) => {
|
||||
const score = await new Parse.Query('Score')
|
||||
.equalTo('player', user)
|
||||
.distinct('score', { useMasterKey: true });
|
||||
expect(score).toEqual([1]);
|
||||
}, { useMasterKey: true });
|
||||
});
|
||||
|
||||
describe_only_db('mongo')('query nested keys', () => {
|
||||
it('queries nested key using equalTo', async () => {
|
||||
const child = new Parse.Object('Child');
|
||||
child.set('key', 'value');
|
||||
await child.save();
|
||||
|
||||
const parent = new Parse.Object('Parent');
|
||||
parent.set('some', {
|
||||
nested: {
|
||||
key: {
|
||||
child,
|
||||
},
|
||||
},
|
||||
});
|
||||
await parent.save();
|
||||
|
||||
const query1 = await new Parse.Query('Parent')
|
||||
.equalTo('some.nested.key.child', child)
|
||||
.find();
|
||||
|
||||
expect(query1.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('queries nested key using containedIn', async () => {
|
||||
const child = new Parse.Object('Child');
|
||||
child.set('key', 'value');
|
||||
await child.save();
|
||||
|
||||
const parent = new Parse.Object('Parent');
|
||||
parent.set('some', {
|
||||
nested: {
|
||||
key: {
|
||||
child,
|
||||
},
|
||||
},
|
||||
});
|
||||
await parent.save();
|
||||
|
||||
const query1 = await new Parse.Query('Parent')
|
||||
.containedIn('some.nested.key.child', [child])
|
||||
.find();
|
||||
|
||||
expect(query1.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('queries nested key using matchesQuery', async () => {
|
||||
const child = new Parse.Object('Child');
|
||||
child.set('key', 'value');
|
||||
await child.save();
|
||||
|
||||
const parent = new Parse.Object('Parent');
|
||||
parent.set('some', {
|
||||
nested: {
|
||||
key: {
|
||||
child,
|
||||
},
|
||||
},
|
||||
});
|
||||
await parent.save();
|
||||
|
||||
const query1 = await new Parse.Query('Parent')
|
||||
.matchesQuery('some.nested.key.child', new Parse.Query('Child').equalTo('key', 'value'))
|
||||
.find();
|
||||
|
||||
expect(query1.length).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -126,7 +126,7 @@ describe('Parse Role testing', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should not recursively load the same role multiple times', done => {
|
||||
it_id('b03abe32-e8e4-4666-9b81-9c804aa53400')(it)('should not recursively load the same role multiple times', done => {
|
||||
const rootRole = 'RootRole';
|
||||
const roleNames = ['FooRole', 'BarRole', 'BazRole'];
|
||||
const allRoles = [rootRole].concat(roleNames);
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
'use strict';
|
||||
/* Tests for ParseServer.js */
|
||||
const express = require('express');
|
||||
const MongoStorageAdapter = require('../lib/Adapters/Storage/Mongo/MongoStorageAdapter').default;
|
||||
const PostgresStorageAdapter = require('../lib/Adapters/Storage/Postgres/PostgresStorageAdapter')
|
||||
.default;
|
||||
const ParseServer = require('../lib/ParseServer').default;
|
||||
const path = require('path');
|
||||
const { spawn } = require('child_process');
|
||||
@@ -45,49 +42,6 @@ describe('Server Url Checks', () => {
|
||||
);
|
||||
});
|
||||
|
||||
xit('handleShutdown, close connection', done => {
|
||||
const mongoURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase';
|
||||
const postgresURI = 'postgres://localhost:5432/parse_server_postgres_adapter_test_database';
|
||||
let databaseAdapter;
|
||||
if (process.env.PARSE_SERVER_TEST_DB === 'postgres') {
|
||||
databaseAdapter = new PostgresStorageAdapter({
|
||||
uri: process.env.PARSE_SERVER_TEST_DATABASE_URI || postgresURI,
|
||||
collectionPrefix: 'test_',
|
||||
});
|
||||
} else {
|
||||
databaseAdapter = new MongoStorageAdapter({
|
||||
uri: mongoURI,
|
||||
collectionPrefix: 'test_',
|
||||
});
|
||||
}
|
||||
let close = false;
|
||||
const newConfiguration = Object.assign({}, defaultConfiguration, {
|
||||
databaseAdapter,
|
||||
serverStartComplete: () => {
|
||||
let promise = Promise.resolve();
|
||||
if (process.env.PARSE_SERVER_TEST_DB !== 'postgres') {
|
||||
promise = parseServer.config.filesController.adapter._connect();
|
||||
}
|
||||
promise.then(() => {
|
||||
parseServer.handleShutdown();
|
||||
parseServer.server.close(err => {
|
||||
if (err) {
|
||||
done.fail('Close Server Error');
|
||||
}
|
||||
reconfigureServer({}).then(() => {
|
||||
expect(close).toBe(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
serverCloseComplete: () => {
|
||||
close = true;
|
||||
},
|
||||
});
|
||||
const parseServer = ParseServer.startApp(newConfiguration);
|
||||
});
|
||||
|
||||
it('does not have unhandled promise rejection in the case of load error', done => {
|
||||
const parseServerProcess = spawn(path.resolve(__dirname, './support/FailingServer.js'));
|
||||
let stdout;
|
||||
|
||||
@@ -2,16 +2,17 @@ const ParseServerRESTController = require('../lib/ParseServerRESTController')
|
||||
.ParseServerRESTController;
|
||||
const ParseServer = require('../lib/ParseServer').default;
|
||||
const Parse = require('parse/node').Parse;
|
||||
const TestUtils = require('../lib/TestUtils');
|
||||
|
||||
let RESTController;
|
||||
|
||||
describe('ParseServerRESTController', () => {
|
||||
let createSpy;
|
||||
beforeEach(() => {
|
||||
RESTController = ParseServerRESTController(
|
||||
Parse.applicationId,
|
||||
ParseServer.promiseRouter({ appId: Parse.applicationId })
|
||||
);
|
||||
createSpy = spyOn(databaseAdapter, 'createObject').and.callThrough();
|
||||
});
|
||||
|
||||
it('should handle a get request', async () => {
|
||||
@@ -133,24 +134,11 @@ describe('ParseServerRESTController', () => {
|
||||
process.env.PARSE_SERVER_TEST_DB === 'postgres'
|
||||
) {
|
||||
describe('transactions', () => {
|
||||
beforeEach(async () => {
|
||||
await TestUtils.destroyAllDataPermanently(true);
|
||||
if (process.env.MONGODB_TOPOLOGY === 'replicaset') {
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI:
|
||||
'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase?replicaSet=replicaset',
|
||||
});
|
||||
} else {
|
||||
await reconfigureServer();
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle a batch request with transaction = true', async () => {
|
||||
const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections
|
||||
await myObject.save();
|
||||
await myObject.destroy();
|
||||
spyOn(databaseAdapter, 'createObject').and.callThrough();
|
||||
createSpy.calls.reset();
|
||||
const response = await RESTController.request('POST', 'batch', {
|
||||
requests: [
|
||||
{
|
||||
@@ -173,10 +161,10 @@ describe('ParseServerRESTController', () => {
|
||||
expect(response[1].success.createdAt).toBeDefined();
|
||||
const query = new Parse.Query('MyObject');
|
||||
const results = await query.find();
|
||||
expect(databaseAdapter.createObject.calls.count() % 2).toBe(0);
|
||||
for (let i = 0; i + 1 < databaseAdapter.createObject.calls.length; i = i + 2) {
|
||||
expect(databaseAdapter.createObject.calls.argsFor(i)[3]).toBe(
|
||||
databaseAdapter.createObject.calls.argsFor(i + 1)[3]
|
||||
expect(createSpy.calls.count()).toBe(2);
|
||||
for (let i = 0; i + 1 < createSpy.calls.length; i = i + 2) {
|
||||
expect(createSpy.calls.argsFor(i)[3]).toBe(
|
||||
createSpy.calls.argsFor(i + 1)[3]
|
||||
);
|
||||
}
|
||||
expect(results.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']);
|
||||
@@ -184,9 +172,11 @@ describe('ParseServerRESTController', () => {
|
||||
|
||||
it('should not save anything when one operation fails in a transaction', async () => {
|
||||
const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections
|
||||
await myObject.save();
|
||||
await myObject.save({ key: 'stringField' });
|
||||
await myObject.destroy();
|
||||
createSpy.calls.reset();
|
||||
try {
|
||||
// Saving a number to a string field should fail
|
||||
await RESTController.request('POST', 'batch', {
|
||||
requests: [
|
||||
{
|
||||
@@ -292,22 +282,22 @@ describe('ParseServerRESTController', () => {
|
||||
});
|
||||
|
||||
it('should generate separate session for each call', async () => {
|
||||
await reconfigureServer();
|
||||
const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections
|
||||
await myObject.save();
|
||||
await myObject.save({ key: 'stringField' });
|
||||
await myObject.destroy();
|
||||
|
||||
const myObject2 = new Parse.Object('MyObject2'); // This is important because transaction only works on pre-existing collections
|
||||
await myObject2.save();
|
||||
await myObject2.save({ key: 'stringField' });
|
||||
await myObject2.destroy();
|
||||
|
||||
spyOn(databaseAdapter, 'createObject').and.callThrough();
|
||||
createSpy.calls.reset();
|
||||
|
||||
let myObjectCalls = 0;
|
||||
Parse.Cloud.beforeSave('MyObject', async () => {
|
||||
myObjectCalls++;
|
||||
if (myObjectCalls === 2) {
|
||||
try {
|
||||
// Saving a number to a string field should fail
|
||||
await RESTController.request('POST', 'batch', {
|
||||
requests: [
|
||||
{
|
||||
@@ -459,14 +449,14 @@ describe('ParseServerRESTController', () => {
|
||||
const results3 = await query3.find();
|
||||
expect(results3.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']);
|
||||
|
||||
expect(databaseAdapter.createObject.calls.count() >= 13).toEqual(true);
|
||||
expect(createSpy.calls.count() >= 13).toEqual(true);
|
||||
let transactionalSession;
|
||||
let transactionalSession2;
|
||||
let myObjectDBCalls = 0;
|
||||
let myObject2DBCalls = 0;
|
||||
let myObject3DBCalls = 0;
|
||||
for (let i = 0; i < databaseAdapter.createObject.calls.count(); i++) {
|
||||
const args = databaseAdapter.createObject.calls.argsFor(i);
|
||||
for (let i = 0; i < createSpy.calls.count(); i++) {
|
||||
const args = createSpy.calls.argsFor(i);
|
||||
switch (args[0]) {
|
||||
case 'MyObject':
|
||||
myObjectDBCalls++;
|
||||
|
||||
@@ -2257,7 +2257,7 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
|
||||
describe('case insensitive signup not allowed', () => {
|
||||
it('signup should fail with duplicate case insensitive username with basic setter', async () => {
|
||||
it_id('464eddc2-7a46-413d-888e-b43b040f1511')(it)('signup should fail with duplicate case insensitive username with basic setter', async () => {
|
||||
const user = new Parse.User();
|
||||
user.set('username', 'test1');
|
||||
user.set('password', 'test');
|
||||
@@ -2271,7 +2271,7 @@ describe('Parse.User testing', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('signup should fail with duplicate case insensitive username with field specific setter', async () => {
|
||||
it_id('1cef005b-d5f0-4699-af0c-bb0af27d2437')(it)('signup should fail with duplicate case insensitive username with field specific setter', async () => {
|
||||
const user = new Parse.User();
|
||||
user.setUsername('test1');
|
||||
user.setPassword('test');
|
||||
@@ -2285,7 +2285,7 @@ describe('Parse.User testing', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('signup should fail with duplicate case insensitive email', async () => {
|
||||
it_id('12735529-98d1-42c0-b437-3b47fe78ddde')(it)('signup should fail with duplicate case insensitive email', async () => {
|
||||
const user = new Parse.User();
|
||||
user.setUsername('test1');
|
||||
user.setPassword('test');
|
||||
@@ -2301,7 +2301,7 @@ describe('Parse.User testing', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('edit should fail with duplicate case insensitive email', async () => {
|
||||
it_id('66e51d52-2420-4b62-8a0d-c7e1b384763e')(it)('edit should fail with duplicate case insensitive email', async () => {
|
||||
const user = new Parse.User();
|
||||
user.setUsername('test1');
|
||||
user.setPassword('test');
|
||||
@@ -2952,7 +2952,7 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should send email when upgrading from anon', async done => {
|
||||
it_id('1be98368-19ac-4c77-8531-762a114f43fb')(it)('should send email when upgrading from anon', async done => {
|
||||
await reconfigureServer();
|
||||
let emailCalled = false;
|
||||
let emailOptions;
|
||||
@@ -3018,7 +3018,7 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should not send email when email is not a string', async done => {
|
||||
it_id('bf668670-39fa-44d3-a9a9-cad52f36d272')(it)('should not send email when email is not a string', async done => {
|
||||
let emailCalled = false;
|
||||
let emailOptions;
|
||||
const emailAdapter = {
|
||||
@@ -3663,6 +3663,7 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
|
||||
xit('should not send a verification email if the user signed up using oauth', done => {
|
||||
pending('this test fails. See: https://github.com/parse-community/parse-server/issues/5097');
|
||||
let emailCalledCount = 0;
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: () => {
|
||||
@@ -3691,7 +3692,7 @@ describe('Parse.User testing', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
}).pend('this test fails. See: https://github.com/parse-community/parse-server/issues/5097');
|
||||
});
|
||||
|
||||
it('should be able to update user with authData passed', done => {
|
||||
let objectId;
|
||||
|
||||
+27
-27
@@ -3,7 +3,7 @@
|
||||
const request = require('../lib/request');
|
||||
|
||||
describe('Password Policy: ', () => {
|
||||
it('should show the invalid link page if the user clicks on the password reset link after the token expires', done => {
|
||||
it_id('b400a867-9f05-496f-af79-933aa588dde5')(it)('should show the invalid link page if the user clicks on the password reset link after the token expires', done => {
|
||||
const user = new Parse.User();
|
||||
let sendEmailOptions;
|
||||
const emailAdapter = {
|
||||
@@ -107,7 +107,7 @@ describe('Password Policy: ', () => {
|
||||
})
|
||||
.then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&username=testResetTokenValidity/;
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&/;
|
||||
expect(response.text.match(re)).not.toBe(null);
|
||||
done();
|
||||
})
|
||||
@@ -150,7 +150,7 @@ describe('Password Policy: ', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should keep reset token with resetTokenReuseIfValid', async done => {
|
||||
it_id('7d98e1f2-ae89-4038-9ea7-5254854ea42e')(it)('should keep reset token with resetTokenReuseIfValid', async done => {
|
||||
const sendEmailOptions = [];
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
@@ -622,7 +622,7 @@ describe('Password Policy: ', () => {
|
||||
})
|
||||
.then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/;
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/;
|
||||
const match = response.text.match(re);
|
||||
if (!match) {
|
||||
fail('should have a token');
|
||||
@@ -634,7 +634,7 @@ describe('Password Policy: ', () => {
|
||||
request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: `new_password=has2init&token=${token}&username=user1`,
|
||||
body: `new_password=has2init&token=${token}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
@@ -645,7 +645,7 @@ describe('Password Policy: ', () => {
|
||||
.then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
expect(response.text).toEqual(
|
||||
'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=user1'
|
||||
'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html'
|
||||
);
|
||||
|
||||
Parse.User.logIn('user1', 'has2init')
|
||||
@@ -714,7 +714,7 @@ describe('Password Policy: ', () => {
|
||||
})
|
||||
.then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/;
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/;
|
||||
const match = response.text.match(re);
|
||||
if (!match) {
|
||||
fail('should have a token');
|
||||
@@ -726,7 +726,7 @@ describe('Password Policy: ', () => {
|
||||
request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: `new_password=hasnodigit&token=${token}&username=user1`,
|
||||
body: `new_password=hasnodigit&token=${token}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
@@ -737,7 +737,7 @@ describe('Password Policy: ', () => {
|
||||
.then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
expect(response.text).toEqual(
|
||||
`Found. Redirecting to http://localhost:8378/1/apps/choose_password?username=user1&token=${token}&id=test&error=Password%20should%20contain%20at%20least%20one%20digit.&app=passwordPolicy`
|
||||
`Found. Redirecting to http://localhost:8378/1/apps/choose_password?token=${token}&id=test&error=Password%20should%20contain%20at%20least%20one%20digit.&app=passwordPolicy`
|
||||
);
|
||||
|
||||
Parse.User.logIn('user1', 'has 1 digit')
|
||||
@@ -900,7 +900,7 @@ describe('Password Policy: ', () => {
|
||||
})
|
||||
.then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/;
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/;
|
||||
const match = response.text.match(re);
|
||||
if (!match) {
|
||||
fail('should have a token');
|
||||
@@ -912,7 +912,7 @@ describe('Password Policy: ', () => {
|
||||
request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: `new_password=xuser12&token=${token}&username=user1`,
|
||||
body: `new_password=xuser12&token=${token}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
@@ -923,7 +923,7 @@ describe('Password Policy: ', () => {
|
||||
.then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
expect(response.text).toEqual(
|
||||
`Found. Redirecting to http://localhost:8378/1/apps/choose_password?username=user1&token=${token}&id=test&error=Password%20cannot%20contain%20your%20username.&app=passwordPolicy`
|
||||
`Found. Redirecting to http://localhost:8378/1/apps/choose_password?token=${token}&id=test&error=Password%20cannot%20contain%20your%20username.&app=passwordPolicy`
|
||||
);
|
||||
|
||||
Parse.User.logIn('user1', 'r@nd0m')
|
||||
@@ -991,7 +991,7 @@ describe('Password Policy: ', () => {
|
||||
resolveWithFullResponse: true,
|
||||
});
|
||||
expect(response.status).toEqual(302);
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/;
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/;
|
||||
const match = response.text.match(re);
|
||||
if (!match) {
|
||||
fail('should have a token');
|
||||
@@ -1003,7 +1003,7 @@ describe('Password Policy: ', () => {
|
||||
await request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: `new_password=xuser12&token=${token}&username=user1`,
|
||||
body: `new_password=xuser12&token=${token}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
@@ -1051,7 +1051,7 @@ describe('Password Policy: ', () => {
|
||||
})
|
||||
.then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/;
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/;
|
||||
const match = response.text.match(re);
|
||||
if (!match) {
|
||||
fail('should have a token');
|
||||
@@ -1063,7 +1063,7 @@ describe('Password Policy: ', () => {
|
||||
request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: `new_password=uuser11&token=${token}&username=user1`,
|
||||
body: `new_password=uuser11&token=${token}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
@@ -1074,7 +1074,7 @@ describe('Password Policy: ', () => {
|
||||
.then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
expect(response.text).toEqual(
|
||||
'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=user1'
|
||||
'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html'
|
||||
);
|
||||
|
||||
Parse.User.logIn('user1', 'uuser11')
|
||||
@@ -1164,7 +1164,7 @@ describe('Password Policy: ', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should succeed if logged in before password expires', done => {
|
||||
it_id('d7d0a93e-efe6-48c0-b622-0f7fb570ccc1')(it)('should succeed if logged in before password expires', done => {
|
||||
const user = new Parse.User();
|
||||
reconfigureServer({
|
||||
appName: 'passwordPolicy',
|
||||
@@ -1197,7 +1197,7 @@ describe('Password Policy: ', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail if logged in after password expires', done => {
|
||||
it_id('22428408-8763-445d-9833-2b2d92008f62')(it)('should fail if logged in after password expires', done => {
|
||||
const user = new Parse.User();
|
||||
reconfigureServer({
|
||||
appName: 'passwordPolicy',
|
||||
@@ -1236,7 +1236,7 @@ describe('Password Policy: ', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should apply password expiry policy to existing user upon first login after policy is enabled', done => {
|
||||
it_id('cc97a109-e35f-4f94-b942-3a6134921cdd')(it)('should apply password expiry policy to existing user upon first login after policy is enabled', done => {
|
||||
const user = new Parse.User();
|
||||
reconfigureServer({
|
||||
appName: 'passwordPolicy',
|
||||
@@ -1304,7 +1304,7 @@ describe('Password Policy: ', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should reset password timestamp when password is reset', done => {
|
||||
it_id('d1e6ab9d-c091-4fea-b952-08b7484bfc89')(it)('should reset password timestamp when password is reset', done => {
|
||||
const user = new Parse.User();
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
@@ -1317,7 +1317,7 @@ describe('Password Policy: ', () => {
|
||||
})
|
||||
.then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/;
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/;
|
||||
const match = response.text.match(re);
|
||||
if (!match) {
|
||||
fail('should have a token');
|
||||
@@ -1329,7 +1329,7 @@ describe('Password Policy: ', () => {
|
||||
request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: `new_password=uuser11&token=${token}&username=user1`,
|
||||
body: `new_password=uuser11&token=${token}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
@@ -1340,7 +1340,7 @@ describe('Password Policy: ', () => {
|
||||
.then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
expect(response.text).toEqual(
|
||||
'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=user1'
|
||||
'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html'
|
||||
);
|
||||
|
||||
Parse.User.logIn('user1', 'uuser11')
|
||||
@@ -1472,7 +1472,7 @@ describe('Password Policy: ', () => {
|
||||
})
|
||||
.then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/;
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/;
|
||||
const match = response.text.match(re);
|
||||
if (!match) {
|
||||
fail('should have a token');
|
||||
@@ -1484,7 +1484,7 @@ describe('Password Policy: ', () => {
|
||||
return request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: `new_password=user1&token=${token}&username=user1`,
|
||||
body: `new_password=user1&token=${token}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
@@ -1500,7 +1500,7 @@ describe('Password Policy: ', () => {
|
||||
const token = data[1];
|
||||
expect(response.status).toEqual(302);
|
||||
expect(response.text).toEqual(
|
||||
`Found. Redirecting to http://localhost:8378/1/apps/choose_password?username=user1&token=${token}&id=test&error=New%20password%20should%20not%20be%20the%20same%20as%20last%201%20passwords.&app=passwordPolicy`
|
||||
`Found. Redirecting to http://localhost:8378/1/apps/choose_password?token=${token}&id=test&error=New%20password%20should%20not%20be%20the%20same%20as%20last%201%20passwords.&app=passwordPolicy`
|
||||
);
|
||||
done();
|
||||
return Promise.resolve();
|
||||
|
||||
@@ -226,7 +226,7 @@ describe('Pointer Permissions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should query on pointer permission enabled column', done => {
|
||||
it_id('f38c35e7-d804-4d32-986d-2579e25d2461')(it)('should query on pointer permission enabled column', done => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const user = new Parse.User();
|
||||
const user2 = new Parse.User();
|
||||
@@ -1070,7 +1070,7 @@ describe('Pointer Permissions', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should work with write', async done => {
|
||||
it_id('1bbb9ed6-5558-4ce5-a238-b1a2015d273f')(it)('should work with write', async done => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const user = new Parse.User();
|
||||
const user2 = new Parse.User();
|
||||
@@ -1203,7 +1203,7 @@ describe('Pointer Permissions', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should query on pointer permission enabled column', async done => {
|
||||
it_id('8a7d188c-b75c-4eac-90b6-9b0b11f873ae')(it)('should query on pointer permission enabled column', async done => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const user = new Parse.User();
|
||||
const user2 = new Parse.User();
|
||||
@@ -2058,7 +2058,7 @@ describe('Pointer Permissions', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should fail for user not listed', async done => {
|
||||
it_id('9ba681d5-59f5-4996-b36d-6647d23e6a44')(it)('should fail for user not listed', async done => {
|
||||
await updateCLP({
|
||||
get: {
|
||||
pointerFields: ['owner'],
|
||||
@@ -2218,7 +2218,7 @@ describe('Pointer Permissions', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should fail for user not listed', async done => {
|
||||
it_id('bcdb158d-c0b6-45e3-84ab-a3636f7cb470')(it)('should fail for user not listed', async done => {
|
||||
await updateCLP({
|
||||
update: {
|
||||
pointerFields: ['owner'],
|
||||
@@ -2270,7 +2270,7 @@ describe('Pointer Permissions', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should fail for user not listed', async done => {
|
||||
it_id('70aa3853-6e26-4c38-a927-2ddb24ced7d4')(it)('should fail for user not listed', async done => {
|
||||
await updateCLP({
|
||||
delete: {
|
||||
pointerFields: ['owner'],
|
||||
@@ -2517,7 +2517,7 @@ describe('Pointer Permissions', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should fail for user not listed', async done => {
|
||||
it_id('84a42339-c7b5-4735-a431-57b46535b073')(it)('should fail for user not listed', async done => {
|
||||
await updateCLP({
|
||||
get: {
|
||||
pointerFields: ['moderators'],
|
||||
@@ -2685,7 +2685,7 @@ describe('Pointer Permissions', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should be allowed (multiple users in array)', async done => {
|
||||
it_id('2b19234a-a471-48b4-bd1a-27bd286d066f')(it)('should be allowed (multiple users in array)', async done => {
|
||||
await updateCLP({
|
||||
update: {
|
||||
pointerFields: ['moderators'],
|
||||
@@ -2698,7 +2698,7 @@ describe('Pointer Permissions', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should fail for user not listed', async done => {
|
||||
it_id('1abb9f4a-fb24-48c7-8025-3001d6cf8737')(it)('should fail for user not listed', async done => {
|
||||
await updateCLP({
|
||||
update: {
|
||||
pointerFields: ['moderators'],
|
||||
@@ -2764,7 +2764,7 @@ describe('Pointer Permissions', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should fail for user not listed', async done => {
|
||||
it_id('3175a0e3-e51e-4b84-a2e6-50bbcc582123')(it)('should fail for user not listed', async done => {
|
||||
await updateCLP({
|
||||
delete: {
|
||||
pointerFields: ['owners'],
|
||||
@@ -2874,7 +2874,7 @@ describe('Pointer Permissions', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should be restricted when updating object without addField permission', async done => {
|
||||
it_id('51e896e9-73b3-404f-b5ff-bdb99005a9f7')(it)('should be restricted when updating object without addField permission', async done => {
|
||||
await updateCLP({
|
||||
update: {
|
||||
'*': true,
|
||||
@@ -2946,7 +2946,7 @@ describe('Pointer Permissions', () => {
|
||||
await initialize();
|
||||
});
|
||||
|
||||
it('should not limit the scope of grouped read permissions', async done => {
|
||||
it_id('b43db366-8cce-4a11-9cf2-eeee9603d40b')(it)('should not limit the scope of grouped read permissions', async done => {
|
||||
await updateCLP({
|
||||
get: {
|
||||
pointerFields: ['owner'],
|
||||
@@ -2967,7 +2967,7 @@ describe('Pointer Permissions', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should not limit the scope of grouped write permissions', async done => {
|
||||
it_id('bbb1686d-0e2a-4365-8b64-b5faa3e7b9cf')(it)('should not limit the scope of grouped write permissions', async done => {
|
||||
await updateCLP({
|
||||
update: {
|
||||
pointerFields: ['owner'],
|
||||
|
||||
+6
-28
@@ -10,28 +10,6 @@ const request = function (url, callback) {
|
||||
};
|
||||
|
||||
describe('public API', () => {
|
||||
it('should return missing username error on ajax request without username provided', async () => {
|
||||
await reconfigureServer({
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
});
|
||||
|
||||
try {
|
||||
await req({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: `new_password=user1&token=43634643&username=`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
followRedirects: false,
|
||||
});
|
||||
} catch (error) {
|
||||
expect(error.status).not.toBe(302);
|
||||
expect(error.text).toEqual('{"code":200,"error":"Missing username"}');
|
||||
}
|
||||
});
|
||||
|
||||
it('should return missing token error on ajax request without token provided', async () => {
|
||||
await reconfigureServer({
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
@@ -41,7 +19,7 @@ describe('public API', () => {
|
||||
await req({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: `new_password=user1&token=&username=Johnny`,
|
||||
body: `new_password=user1&token=`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
@@ -63,7 +41,7 @@ describe('public API', () => {
|
||||
await req({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: `new_password=&token=132414&username=Johnny`,
|
||||
body: `new_password=&token=132414`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
@@ -111,8 +89,8 @@ describe('public API', () => {
|
||||
});
|
||||
|
||||
describe('public API without publicServerURL', () => {
|
||||
beforeEach(done => {
|
||||
reconfigureServer({ appName: 'unused' }).then(done, fail);
|
||||
beforeEach(async () => {
|
||||
await reconfigureServer({ appName: 'unused' });
|
||||
});
|
||||
it('should get 404 on verify_email', done => {
|
||||
request('http://localhost:8378/1/apps/test/verify_email', (err, httpResponse) => {
|
||||
@@ -137,8 +115,8 @@ describe('public API without publicServerURL', () => {
|
||||
});
|
||||
|
||||
describe('public API supplied with invalid application id', () => {
|
||||
beforeEach(done => {
|
||||
reconfigureServer({ appName: 'unused' }).then(done, fail);
|
||||
beforeEach(async () => {
|
||||
await reconfigureServer({ appName: 'unused' });
|
||||
});
|
||||
|
||||
it('should get 403 on verify_email', done => {
|
||||
|
||||
@@ -23,13 +23,8 @@ function createProduct() {
|
||||
}
|
||||
|
||||
describe('test validate_receipt endpoint', () => {
|
||||
beforeEach(done => {
|
||||
createProduct()
|
||||
.then(done)
|
||||
.catch(function (err) {
|
||||
console.error({ err });
|
||||
done();
|
||||
});
|
||||
beforeEach(async () => {
|
||||
await createProduct();
|
||||
});
|
||||
|
||||
it('should bypass appstore validation', async () => {
|
||||
|
||||
+55
-66
@@ -168,7 +168,7 @@ describe('PushController', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('properly increment badges', async () => {
|
||||
it_id('01e3e1b8-fad2-4249-b664-5a3efaab8cb1')(it)('properly increment badges', async () => {
|
||||
const pushAdapter = {
|
||||
send: function (body, installations) {
|
||||
const badge = body.data.badge;
|
||||
@@ -182,6 +182,9 @@ describe('PushController', () => {
|
||||
return ['ios', 'android'];
|
||||
},
|
||||
};
|
||||
await reconfigureServer({
|
||||
push: { adapter: pushAdapter },
|
||||
});
|
||||
const payload = {
|
||||
data: {
|
||||
alert: 'Hello World!',
|
||||
@@ -212,9 +215,6 @@ describe('PushController', () => {
|
||||
const auth = {
|
||||
isMaster: true,
|
||||
};
|
||||
await reconfigureServer({
|
||||
push: { adapter: pushAdapter },
|
||||
});
|
||||
await Parse.Object.saveAll(installations);
|
||||
const pushStatusId = await sendPush(payload, {}, config, auth);
|
||||
await pushCompleted(pushStatusId);
|
||||
@@ -233,7 +233,7 @@ describe('PushController', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('properly increment badges by more than 1', async () => {
|
||||
it_id('14afcedf-e65d-41cd-981e-07f32df84c14')(it)('properly increment badges by more than 1', async () => {
|
||||
const pushAdapter = {
|
||||
send: function (body, installations) {
|
||||
const badge = body.data.badge;
|
||||
@@ -247,6 +247,9 @@ describe('PushController', () => {
|
||||
return ['ios', 'android'];
|
||||
},
|
||||
};
|
||||
await reconfigureServer({
|
||||
push: { adapter: pushAdapter },
|
||||
});
|
||||
const payload = {
|
||||
data: {
|
||||
alert: 'Hello World!',
|
||||
@@ -277,9 +280,6 @@ describe('PushController', () => {
|
||||
const auth = {
|
||||
isMaster: true,
|
||||
};
|
||||
await reconfigureServer({
|
||||
push: { adapter: pushAdapter },
|
||||
});
|
||||
await Parse.Object.saveAll(installations);
|
||||
const pushStatusId = await sendPush(payload, {}, config, auth);
|
||||
await pushCompleted(pushStatusId);
|
||||
@@ -295,7 +295,7 @@ describe('PushController', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('properly set badges to 1', async () => {
|
||||
it_id('758dd579-aa91-4010-9033-8d48d3463644')(it)('properly set badges to 1', async () => {
|
||||
const pushAdapter = {
|
||||
send: function (body, installations) {
|
||||
const badge = body.data.badge;
|
||||
@@ -309,7 +309,9 @@ describe('PushController', () => {
|
||||
return ['ios'];
|
||||
},
|
||||
};
|
||||
|
||||
await reconfigureServer({
|
||||
push: { adapter: pushAdapter },
|
||||
});
|
||||
const payload = {
|
||||
data: {
|
||||
alert: 'Hello World!',
|
||||
@@ -331,9 +333,6 @@ describe('PushController', () => {
|
||||
const auth = {
|
||||
isMaster: true,
|
||||
};
|
||||
await reconfigureServer({
|
||||
push: { adapter: pushAdapter },
|
||||
});
|
||||
await Parse.Object.saveAll(installations);
|
||||
const pushStatusId = await sendPush(payload, {}, config, auth);
|
||||
await pushCompleted(pushStatusId);
|
||||
@@ -350,7 +349,7 @@ describe('PushController', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('properly set badges to 1 with complex query #2903 #3022', async () => {
|
||||
it_id('75c39ae3-06ac-4354-b321-931e81c5a927')(it)('properly set badges to 1 with complex query #2903 #3022', async () => {
|
||||
const payload = {
|
||||
data: {
|
||||
alert: 'Hello World!',
|
||||
@@ -382,14 +381,13 @@ describe('PushController', () => {
|
||||
return ['ios'];
|
||||
},
|
||||
};
|
||||
|
||||
await reconfigureServer({
|
||||
push: { adapter: pushAdapter },
|
||||
});
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const auth = {
|
||||
isMaster: true,
|
||||
};
|
||||
await reconfigureServer({
|
||||
push: { adapter: pushAdapter },
|
||||
});
|
||||
await Parse.Object.saveAll(installations);
|
||||
const objectIds = installations.map(installation => {
|
||||
return installation.id;
|
||||
@@ -406,7 +404,7 @@ describe('PushController', () => {
|
||||
expect(results.length).toBe(5);
|
||||
});
|
||||
|
||||
it('properly creates _PushStatus', async () => {
|
||||
it_id('667f31c0-b458-4f61-ab57-668c04e3cc0b')(it)('properly creates _PushStatus', async () => {
|
||||
const pushStatusAfterSave = {
|
||||
handler: function () {},
|
||||
};
|
||||
@@ -445,14 +443,13 @@ describe('PushController', () => {
|
||||
return ['ios'];
|
||||
},
|
||||
};
|
||||
|
||||
await reconfigureServer({
|
||||
push: { adapter: pushAdapter },
|
||||
});
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const auth = {
|
||||
isMaster: true,
|
||||
};
|
||||
await reconfigureServer({
|
||||
push: { adapter: pushAdapter },
|
||||
});
|
||||
await Parse.Object.saveAll(installations);
|
||||
const pushStatusId = await sendPush(payload, {}, config, auth);
|
||||
await pushCompleted(pushStatusId);
|
||||
@@ -521,7 +518,7 @@ describe('PushController', () => {
|
||||
expect(succeedCount).toBe(1);
|
||||
});
|
||||
|
||||
it('properly creates _PushStatus without serverURL', async () => {
|
||||
it_id('30e0591a-56de-4720-8c60-7d72291b532a')(it)('properly creates _PushStatus without serverURL', async () => {
|
||||
const pushStatusAfterSave = {
|
||||
handler: function () {},
|
||||
};
|
||||
@@ -548,16 +545,15 @@ describe('PushController', () => {
|
||||
return ['ios'];
|
||||
},
|
||||
};
|
||||
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const auth = {
|
||||
isMaster: true,
|
||||
};
|
||||
await installation.save();
|
||||
await reconfigureServer({
|
||||
serverURL: 'http://localhost:8378/', // server with borked URL
|
||||
push: { adapter: pushAdapter },
|
||||
});
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const auth = {
|
||||
isMaster: true,
|
||||
};
|
||||
const pushStatusId = await sendPush(payload, {}, config, auth);
|
||||
// it is enqueued so it can take time
|
||||
await jasmine.timeout(1000);
|
||||
@@ -580,6 +576,9 @@ describe('PushController', () => {
|
||||
return ['ios'];
|
||||
},
|
||||
};
|
||||
await reconfigureServer({
|
||||
push: { adapter: pushAdapter },
|
||||
});
|
||||
// $ins is invalid query
|
||||
const where = {
|
||||
channels: {
|
||||
@@ -596,9 +595,6 @@ describe('PushController', () => {
|
||||
isMaster: true,
|
||||
};
|
||||
const pushController = new PushController();
|
||||
await reconfigureServer({
|
||||
push: { adapter: pushAdapter },
|
||||
});
|
||||
const config = Config.get(Parse.applicationId);
|
||||
try {
|
||||
await pushController.sendPush(payload, where, config, auth);
|
||||
@@ -615,7 +611,7 @@ describe('PushController', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should support full RESTQuery for increment', async () => {
|
||||
it_id('53551fc3-b975-4774-92e6-7e5f3c05e105')(it)('should support full RESTQuery for increment', async () => {
|
||||
const payload = {
|
||||
data: {
|
||||
alert: 'Hello World!',
|
||||
@@ -631,6 +627,9 @@ describe('PushController', () => {
|
||||
return ['ios'];
|
||||
},
|
||||
};
|
||||
await reconfigureServer({
|
||||
push: { adapter: pushAdapter },
|
||||
});
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const auth = {
|
||||
isMaster: true,
|
||||
@@ -641,9 +640,6 @@ describe('PushController', () => {
|
||||
$in: ['device_token_0', 'device_token_1', 'device_token_2'],
|
||||
},
|
||||
};
|
||||
await reconfigureServer({
|
||||
push: { adapter: pushAdapter },
|
||||
});
|
||||
const installations = [];
|
||||
while (installations.length != 5) {
|
||||
const installation = new Parse.Object('_Installation');
|
||||
@@ -678,7 +674,9 @@ describe('PushController', () => {
|
||||
return ['ios'];
|
||||
},
|
||||
};
|
||||
|
||||
await reconfigureServer({
|
||||
push: { adapter: pushAdapter },
|
||||
});
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const auth = {
|
||||
isMaster: true,
|
||||
@@ -686,9 +684,6 @@ describe('PushController', () => {
|
||||
const where = {
|
||||
deviceType: 'ios',
|
||||
};
|
||||
await reconfigureServer({
|
||||
push: { adapter: pushAdapter },
|
||||
});
|
||||
const installations = [];
|
||||
while (installations.length != 5) {
|
||||
const installation = new Parse.Object('_Installation');
|
||||
@@ -762,10 +757,6 @@ describe('PushController', () => {
|
||||
});
|
||||
|
||||
it('should not schedule push when not configured', async () => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const auth = {
|
||||
isMaster: true,
|
||||
};
|
||||
const pushAdapter = {
|
||||
send: function (body, installations) {
|
||||
return successfulTransmissions(body, installations);
|
||||
@@ -774,7 +765,13 @@ describe('PushController', () => {
|
||||
return ['ios'];
|
||||
},
|
||||
};
|
||||
|
||||
await reconfigureServer({
|
||||
push: { adapter: pushAdapter },
|
||||
});
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const auth = {
|
||||
isMaster: true,
|
||||
};
|
||||
const pushController = new PushController();
|
||||
const payload = {
|
||||
data: {
|
||||
@@ -793,10 +790,6 @@ describe('PushController', () => {
|
||||
installation.set('deviceType', 'ios');
|
||||
installations.push(installation);
|
||||
}
|
||||
|
||||
await reconfigureServer({
|
||||
push: { adapter: pushAdapter },
|
||||
});
|
||||
await Parse.Object.saveAll(installations);
|
||||
await pushController.sendPush(payload, {}, config, auth);
|
||||
await jasmine.timeout(1000);
|
||||
@@ -986,6 +979,10 @@ describe('PushController', () => {
|
||||
return ['ios'];
|
||||
},
|
||||
};
|
||||
spyOn(pushAdapter, 'send').and.callThrough();
|
||||
await reconfigureServer({
|
||||
push: { adapter: pushAdapter },
|
||||
});
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const auth = {
|
||||
isMaster: true,
|
||||
@@ -1007,10 +1004,6 @@ describe('PushController', () => {
|
||||
installations[1].set('localeIdentifier', 'fr-FR');
|
||||
installations[2].set('localeIdentifier', 'en-US');
|
||||
|
||||
spyOn(pushAdapter, 'send').and.callThrough();
|
||||
await reconfigureServer({
|
||||
push: { adapter: pushAdapter },
|
||||
});
|
||||
await Parse.Object.saveAll(installations);
|
||||
const pushStatusId = await sendPush(payload, where, config, auth);
|
||||
await pushCompleted(pushStatusId);
|
||||
@@ -1030,7 +1023,7 @@ describe('PushController', () => {
|
||||
// No installation is in es so only 1 call for fr, and another for default
|
||||
});
|
||||
|
||||
it('should update audiences', async () => {
|
||||
it_id('ef2e5569-50c3-40c2-ab49-175cdbd5f024')(it)('should update audiences', async () => {
|
||||
const pushAdapter = {
|
||||
send: function (body, installations) {
|
||||
return successfulTransmissions(body, installations);
|
||||
@@ -1039,7 +1032,10 @@ describe('PushController', () => {
|
||||
return ['ios'];
|
||||
},
|
||||
};
|
||||
|
||||
spyOn(pushAdapter, 'send').and.callThrough();
|
||||
await reconfigureServer({
|
||||
push: { adapter: pushAdapter },
|
||||
});
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const auth = {
|
||||
isMaster: true,
|
||||
@@ -1060,10 +1056,6 @@ describe('PushController', () => {
|
||||
installation.set('deviceType', 'ios');
|
||||
installations.push(installation);
|
||||
}
|
||||
spyOn(pushAdapter, 'send').and.callThrough();
|
||||
await reconfigureServer({
|
||||
push: { adapter: pushAdapter },
|
||||
});
|
||||
await Parse.Object.saveAll(installations);
|
||||
|
||||
// Create an audience
|
||||
@@ -1224,14 +1216,11 @@ describe('PushController', () => {
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(done => {
|
||||
reconfigureServer({
|
||||
beforeEach(async () => {
|
||||
await reconfigureServer({
|
||||
push: { adapter: pushAdapter },
|
||||
})
|
||||
.then(() => {
|
||||
config = Config.get(Parse.applicationId);
|
||||
})
|
||||
.then(done, done.fail);
|
||||
});
|
||||
config = Config.get(Parse.applicationId);
|
||||
});
|
||||
|
||||
it('should throw if both expiration_time and expiration_interval are set', () => {
|
||||
|
||||
@@ -271,7 +271,7 @@ describe('PushWorker', () => {
|
||||
toAwait.then(done).catch(done);
|
||||
});
|
||||
|
||||
it('tracks push status per UTC offsets', done => {
|
||||
it_id('764d28ab-241b-4b96-8ce9-e03541850e3f')(it)('tracks push status per UTC offsets', done => {
|
||||
const config = Config.get('test');
|
||||
const handler = pushStatusHandler(config);
|
||||
const spy = spyOn(rest, 'update').and.callThrough();
|
||||
|
||||
@@ -3,7 +3,10 @@ const RedisPubSub = require('../lib/Adapters/PubSub/RedisPubSub').RedisPubSub;
|
||||
describe('RedisPubSub', function () {
|
||||
beforeEach(function (done) {
|
||||
// Mock redis
|
||||
const createClient = jasmine.createSpy('createClient');
|
||||
const createClient = jasmine.createSpy('createClient').and.returnValue({
|
||||
connect: jasmine.createSpy('connect').and.resolveTo(),
|
||||
on: jasmine.createSpy('on'),
|
||||
});
|
||||
jasmine.mockLibrary('redis', 'createClient', createClient);
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -16,8 +16,13 @@ const emailAdapter = {
|
||||
const appName = 'test';
|
||||
const publicServerURL = 'http://localhost:8378/1';
|
||||
|
||||
describe('Regex Vulnerabilities', function () {
|
||||
beforeEach(async function () {
|
||||
describe('Regex Vulnerabilities', () => {
|
||||
let objectId;
|
||||
let sessionToken;
|
||||
let partialSessionToken;
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
await reconfigureServer({
|
||||
maintenanceKey: 'test2',
|
||||
verifyUserEmails: true,
|
||||
@@ -38,13 +43,13 @@ describe('Regex Vulnerabilities', function () {
|
||||
email: 'someemail@somedomain.com',
|
||||
}),
|
||||
});
|
||||
this.objectId = signUpResponse.data.objectId;
|
||||
this.sessionToken = signUpResponse.data.sessionToken;
|
||||
this.partialSessionToken = this.sessionToken.slice(0, 3);
|
||||
objectId = signUpResponse.data.objectId;
|
||||
sessionToken = signUpResponse.data.sessionToken;
|
||||
partialSessionToken = sessionToken.slice(0, 3);
|
||||
});
|
||||
|
||||
describe('on session token', function () {
|
||||
it('should not work with regex', async function () {
|
||||
describe('on session token', () => {
|
||||
it('should not work with regex', async () => {
|
||||
try {
|
||||
await request({
|
||||
url: `${serverURL}/users/me`,
|
||||
@@ -53,7 +58,7 @@ describe('Regex Vulnerabilities', function () {
|
||||
body: JSON.stringify({
|
||||
...keys,
|
||||
_SessionToken: {
|
||||
$regex: this.partialSessionToken,
|
||||
$regex: partialSessionToken,
|
||||
},
|
||||
_method: 'GET',
|
||||
}),
|
||||
@@ -65,43 +70,43 @@ describe('Regex Vulnerabilities', function () {
|
||||
}
|
||||
});
|
||||
|
||||
it('should work with plain token', async function () {
|
||||
it('should work with plain token', async () => {
|
||||
const meResponse = await request({
|
||||
url: `${serverURL}/users/me`,
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
...keys,
|
||||
_SessionToken: this.sessionToken,
|
||||
_SessionToken: sessionToken,
|
||||
_method: 'GET',
|
||||
}),
|
||||
});
|
||||
expect(meResponse.data.objectId).toEqual(this.objectId);
|
||||
expect(meResponse.data.sessionToken).toEqual(this.sessionToken);
|
||||
expect(meResponse.data.objectId).toEqual(objectId);
|
||||
expect(meResponse.data.sessionToken).toEqual(sessionToken);
|
||||
});
|
||||
});
|
||||
|
||||
describe('on verify e-mail', function () {
|
||||
describe('on verify e-mail', () => {
|
||||
beforeEach(async function () {
|
||||
const userQuery = new Parse.Query(Parse.User);
|
||||
this.user = await userQuery.get(this.objectId, { useMasterKey: true });
|
||||
user = await userQuery.get(objectId, { useMasterKey: true });
|
||||
});
|
||||
|
||||
it('should not work with regex', async function () {
|
||||
expect(this.user.get('emailVerified')).toEqual(false);
|
||||
it('should not work with regex', async () => {
|
||||
expect(user.get('emailVerified')).toEqual(false);
|
||||
await request({
|
||||
url: `${serverURL}/apps/test/verify_email?username=someemail@somedomain.com&token[$regex]=`,
|
||||
url: `${serverURL}/apps/test/verify_email?token[$regex]=`,
|
||||
method: 'GET',
|
||||
});
|
||||
await this.user.fetch({ useMasterKey: true });
|
||||
expect(this.user.get('emailVerified')).toEqual(false);
|
||||
await user.fetch({ useMasterKey: true });
|
||||
expect(user.get('emailVerified')).toEqual(false);
|
||||
});
|
||||
|
||||
it('should work with plain token', async function () {
|
||||
expect(this.user.get('emailVerified')).toEqual(false);
|
||||
it_id('92bbb86d-bcda-49fa-8d79-aa0501078044')(it)('should work with plain token', async () => {
|
||||
expect(user.get('emailVerified')).toEqual(false);
|
||||
const current = await request({
|
||||
method: 'GET',
|
||||
url: `http://localhost:8378/1/classes/_User/${this.user.id}`,
|
||||
url: `http://localhost:8378/1/classes/_User/${user.id}`,
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
@@ -112,21 +117,21 @@ describe('Regex Vulnerabilities', function () {
|
||||
}).then(res => res.data);
|
||||
// It should work
|
||||
await request({
|
||||
url: `${serverURL}/apps/test/verify_email?username=someemail@somedomain.com&token=${current._email_verify_token}`,
|
||||
url: `${serverURL}/apps/test/verify_email?token=${current._email_verify_token}`,
|
||||
method: 'GET',
|
||||
});
|
||||
await this.user.fetch({ useMasterKey: true });
|
||||
expect(this.user.get('emailVerified')).toEqual(true);
|
||||
await user.fetch({ useMasterKey: true });
|
||||
expect(user.get('emailVerified')).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('on password reset', function () {
|
||||
beforeEach(async function () {
|
||||
this.user = await Parse.User.logIn('someemail@somedomain.com', 'somepassword');
|
||||
describe('on password reset', () => {
|
||||
beforeEach(async () => {
|
||||
user = await Parse.User.logIn('someemail@somedomain.com', 'somepassword');
|
||||
});
|
||||
|
||||
it('should not work with regex', async function () {
|
||||
expect(this.user.id).toEqual(this.objectId);
|
||||
it('should not work with regex', async () => {
|
||||
expect(user.id).toEqual(objectId);
|
||||
await request({
|
||||
url: `${serverURL}/requestPasswordReset`,
|
||||
method: 'POST',
|
||||
@@ -137,9 +142,9 @@ describe('Regex Vulnerabilities', function () {
|
||||
email: 'someemail@somedomain.com',
|
||||
}),
|
||||
});
|
||||
await this.user.fetch({ useMasterKey: true });
|
||||
await user.fetch({ useMasterKey: true });
|
||||
const passwordResetResponse = await request({
|
||||
url: `${serverURL}/apps/test/request_password_reset?username=someemail@somedomain.com&token[$regex]=`,
|
||||
url: `${serverURL}/apps/test/request_password_reset?token[$regex]=`,
|
||||
method: 'GET',
|
||||
});
|
||||
expect(passwordResetResponse.status).toEqual(302);
|
||||
@@ -162,8 +167,8 @@ describe('Regex Vulnerabilities', function () {
|
||||
}
|
||||
});
|
||||
|
||||
it('should work with plain token', async function () {
|
||||
expect(this.user.id).toEqual(this.objectId);
|
||||
it('should work with plain token', async () => {
|
||||
expect(user.id).toEqual(objectId);
|
||||
await request({
|
||||
url: `${serverURL}/requestPasswordReset`,
|
||||
method: 'POST',
|
||||
@@ -176,7 +181,7 @@ describe('Regex Vulnerabilities', function () {
|
||||
});
|
||||
const current = await request({
|
||||
method: 'GET',
|
||||
url: `http://localhost:8378/1/classes/_User/${this.user.id}`,
|
||||
url: `http://localhost:8378/1/classes/_User/${user.id}`,
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
@@ -187,7 +192,7 @@ describe('Regex Vulnerabilities', function () {
|
||||
}).then(res => res.data);
|
||||
const token = current._perishable_token;
|
||||
const passwordResetResponse = await request({
|
||||
url: `${serverURL}/apps/test/request_password_reset?username=someemail@somedomain.com&token=${token}`,
|
||||
url: `${serverURL}/apps/test/request_password_reset?token=${token}`,
|
||||
method: 'GET',
|
||||
});
|
||||
expect(passwordResetResponse.status).toEqual(302);
|
||||
@@ -204,7 +209,7 @@ describe('Regex Vulnerabilities', function () {
|
||||
},
|
||||
});
|
||||
const userAgain = await Parse.User.logIn('someemail@somedomain.com', 'newpassword');
|
||||
expect(userAgain.id).toEqual(this.objectId);
|
||||
expect(userAgain.id).toEqual(objectId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -392,7 +392,7 @@ describe('RestQuery.each', () => {
|
||||
beforeEach(() => {
|
||||
config = Config.get('test');
|
||||
});
|
||||
it('should run each', async () => {
|
||||
it_id('3416c90b-ee2e-4bb5-9231-46cd181cd0a2')(it)('should run each', async () => {
|
||||
const objects = [];
|
||||
while (objects.length != 10) {
|
||||
objects.push(new Parse.Object('Object', { value: objects.length }));
|
||||
@@ -419,7 +419,7 @@ describe('RestQuery.each', () => {
|
||||
expect(results.length).toBe(7);
|
||||
});
|
||||
|
||||
it('should work with query on relations', async () => {
|
||||
it_id('0fe22501-4b18-461e-b87d-82ceac4a496e')(it)('should work with query on relations', async () => {
|
||||
const objectA = new Parse.Object('Letter', { value: 'A' });
|
||||
const objectB = new Parse.Object('Letter', { value: 'B' });
|
||||
|
||||
|
||||
@@ -15,9 +15,9 @@ function createUser() {
|
||||
}
|
||||
|
||||
describe_only_db('mongo')('revocable sessions', () => {
|
||||
beforeEach(done => {
|
||||
beforeEach(async () => {
|
||||
// Create 1 user with the legacy
|
||||
createUser().then(done);
|
||||
await createUser();
|
||||
});
|
||||
|
||||
it('should upgrade legacy session token', done => {
|
||||
|
||||
@@ -309,6 +309,12 @@ describe('SchemaController', () => {
|
||||
foo: { type: 'String' },
|
||||
},
|
||||
classLevelPermissions: {
|
||||
ACL: {
|
||||
'*': {
|
||||
read: true,
|
||||
write: true,
|
||||
},
|
||||
},
|
||||
find: { '*': true },
|
||||
get: { '*': true },
|
||||
count: { '*': true },
|
||||
@@ -329,6 +335,12 @@ describe('SchemaController', () => {
|
||||
|
||||
it('can update classes without needing an object', done => {
|
||||
const levelPermissions = {
|
||||
ACL: {
|
||||
'*': {
|
||||
read: true,
|
||||
write: true,
|
||||
},
|
||||
},
|
||||
find: { '*': true },
|
||||
get: { '*': true },
|
||||
count: { '*': true },
|
||||
@@ -489,6 +501,12 @@ describe('SchemaController', () => {
|
||||
foo: { type: 'String' },
|
||||
},
|
||||
classLevelPermissions: {
|
||||
ACL: {
|
||||
'*': {
|
||||
read: true,
|
||||
write: true,
|
||||
},
|
||||
},
|
||||
find: { '*': true },
|
||||
get: { '*': true },
|
||||
count: { '*': true },
|
||||
@@ -694,6 +712,12 @@ describe('SchemaController', () => {
|
||||
|
||||
it('refuses to add CLP with incorrect find', done => {
|
||||
const levelPermissions = {
|
||||
ACL: {
|
||||
'*': {
|
||||
read: true,
|
||||
write: true,
|
||||
},
|
||||
},
|
||||
find: { '*': false },
|
||||
get: { '*': true },
|
||||
create: { '*': true },
|
||||
@@ -717,6 +741,12 @@ describe('SchemaController', () => {
|
||||
|
||||
it('refuses to add CLP when incorrectly sending a string to protectedFields object value instead of an array', done => {
|
||||
const levelPermissions = {
|
||||
ACL: {
|
||||
'*': {
|
||||
read: true,
|
||||
write: true,
|
||||
},
|
||||
},
|
||||
find: { '*': true },
|
||||
get: { '*': true },
|
||||
create: { '*': true },
|
||||
@@ -785,6 +815,12 @@ describe('SchemaController', () => {
|
||||
aPolygon: { type: 'Polygon' },
|
||||
},
|
||||
classLevelPermissions: {
|
||||
ACL: {
|
||||
'*': {
|
||||
read: true,
|
||||
write: true,
|
||||
},
|
||||
},
|
||||
find: { '*': true },
|
||||
get: { '*': true },
|
||||
count: { '*': true },
|
||||
@@ -832,6 +868,12 @@ describe('SchemaController', () => {
|
||||
parseVersion: { type: 'String' },
|
||||
},
|
||||
classLevelPermissions: {
|
||||
ACL: {
|
||||
'*': {
|
||||
read: true,
|
||||
write: true,
|
||||
},
|
||||
},
|
||||
find: { '*': true },
|
||||
get: { '*': true },
|
||||
count: { '*': true },
|
||||
@@ -866,6 +908,12 @@ describe('SchemaController', () => {
|
||||
roles: { type: 'Relation', targetClass: '_Role' },
|
||||
},
|
||||
classLevelPermissions: {
|
||||
ACL: {
|
||||
'*': {
|
||||
read: true,
|
||||
write: true,
|
||||
},
|
||||
},
|
||||
find: { '*': true },
|
||||
get: { '*': true },
|
||||
count: { '*': true },
|
||||
@@ -900,6 +948,12 @@ describe('SchemaController', () => {
|
||||
ACL: { type: 'ACL' },
|
||||
},
|
||||
classLevelPermissions: {
|
||||
ACL: {
|
||||
'*': {
|
||||
read: true,
|
||||
write: true,
|
||||
},
|
||||
},
|
||||
find: { '*': true },
|
||||
get: { '*': true },
|
||||
count: { '*': true },
|
||||
@@ -1070,6 +1124,12 @@ describe('SchemaController', () => {
|
||||
relationField: { type: 'Relation', targetClass: '_User' },
|
||||
},
|
||||
classLevelPermissions: {
|
||||
ACL: {
|
||||
'*': {
|
||||
read: true,
|
||||
write: true,
|
||||
},
|
||||
},
|
||||
find: { '*': true },
|
||||
get: { '*': true },
|
||||
count: { '*': true },
|
||||
|
||||
@@ -5,10 +5,8 @@ describe('Schema Performance', function () {
|
||||
let config;
|
||||
|
||||
beforeEach(async () => {
|
||||
await reconfigureServer();
|
||||
config = Config.get('test');
|
||||
config.schemaCache.clear();
|
||||
const databaseAdapter = config.database.adapter;
|
||||
await reconfigureServer({ databaseAdapter });
|
||||
getAllSpy = spyOn(databaseAdapter, 'getAllClasses').and.callThrough();
|
||||
});
|
||||
|
||||
@@ -167,6 +165,12 @@ describe('Schema Performance', function () {
|
||||
await schema.reloadData();
|
||||
|
||||
const levelPermissions = {
|
||||
ACL: {
|
||||
'*': {
|
||||
read: true,
|
||||
write: true,
|
||||
},
|
||||
},
|
||||
find: { '*': true },
|
||||
get: { '*': true },
|
||||
create: { '*': true },
|
||||
@@ -205,7 +209,7 @@ describe('Schema Performance', function () {
|
||||
expect(getAllSpy.calls.count()).toBe(2);
|
||||
});
|
||||
|
||||
it('does reload with schemaCacheTtl', async () => {
|
||||
it_id('9dd70965-b683-4cb8-b43a-44c1f4def9f4')(it)('does reload with schemaCacheTtl', async () => {
|
||||
const databaseURI =
|
||||
process.env.PARSE_SERVER_TEST_DB === 'postgres'
|
||||
? process.env.PARSE_SERVER_TEST_DATABASE_URI
|
||||
@@ -241,7 +245,7 @@ describe('Schema Performance', function () {
|
||||
expect(spy.reloadCalls).toBe(1);
|
||||
});
|
||||
|
||||
it('cannot set invalid databaseOptions', async () => {
|
||||
it_id('b0ae21f2-c947-48ed-a0db-e8900d45a4c8')(it)('cannot set invalid databaseOptions', async () => {
|
||||
const expectError = async (key, value, expected) =>
|
||||
expectAsync(
|
||||
reconfigureServer({ databaseAdapter: undefined, databaseOptions: { [key]: value } })
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user