mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b905137278 | ||
|
|
2c19c2e4d4 | ||
|
|
30576f1091 | ||
|
|
e016d813e0 | ||
|
|
c8bc200729 | ||
|
|
09d04b0aad | ||
|
|
38f64be596 |
@@ -6,9 +6,8 @@
|
||||
"presets": [
|
||||
["@babel/preset-env", {
|
||||
"targets": {
|
||||
"node": "14",
|
||||
},
|
||||
"exclude": ["proposal-dynamic-import"]
|
||||
"node": "12"
|
||||
}
|
||||
}]
|
||||
],
|
||||
"sourceMaps": "inline"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
*.md
|
||||
PATENTS
|
||||
LICENSE
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
.gitignore
|
||||
|
||||
+3
-8
@@ -5,14 +5,13 @@
|
||||
"node": true,
|
||||
"es6": true
|
||||
},
|
||||
"parser": "@babel/eslint-parser",
|
||||
"parser": "babel-eslint",
|
||||
"plugins": [
|
||||
"flowtype"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module",
|
||||
"requireConfigFile": false
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"indent": ["error", 2, { "SwitchCase": 1 }],
|
||||
@@ -24,10 +23,6 @@
|
||||
"prefer-const": "error",
|
||||
"space-infix-ops": "error",
|
||||
"no-useless-escape": "off",
|
||||
"require-atomic-updates": "off",
|
||||
"object-curly-spacing": ["error", "always"]
|
||||
},
|
||||
"globals": {
|
||||
"Parse": true
|
||||
"require-atomic-updates": "off"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,4 +8,3 @@
|
||||
|
||||
[options]
|
||||
suppress_comment= \\(.\\|\n\\)*\\@flow-disable-next
|
||||
esproposal.optional_chaining=enable
|
||||
|
||||
@@ -8,10 +8,16 @@ assignees: ''
|
||||
---
|
||||
|
||||
### New Issue Checklist
|
||||
<!--
|
||||
Check every following box [x] before submitting your issue.
|
||||
Click the "Preview" tab for better readability.
|
||||
Thanks for contributing to Parse Platform!
|
||||
-->
|
||||
|
||||
- Report security issues [confidentially](https://github.com/parse-community/parse-server/security/policy).
|
||||
- Any contribution is under this [license](https://github.com/parse-community/parse-server/blob/alpha/LICENSE).
|
||||
- Before posting search [existing issues](https://github.com/parse-community/parse-server/issues?q=is%3Aissue).
|
||||
- [ ] I am not disclosing a [vulnerability](https://github.com/parse-community/parse-server/blob/master/SECURITY.md).
|
||||
- [ ] I am not just asking a [question](https://github.com/parse-community/.github/blob/master/SUPPORT.md).
|
||||
- [ ] I have searched through [existing issues](https://github.com/parse-community/parse-server/issues?q=is%3Aissue).
|
||||
- [ ] I can reproduce the issue with the [latest version of Parse Server](https://github.com/parse-community/parse-server/releases). <!-- We don't investigate issues for outdated releases. -->
|
||||
|
||||
### Issue Description
|
||||
<!-- What is the specific issue with Parse Server? -->
|
||||
@@ -24,7 +30,6 @@ assignees: ''
|
||||
|
||||
### Expected Outcome
|
||||
<!-- What outcome, for example query result, did you expect? -->
|
||||
|
||||
### Environment
|
||||
<!-- Be specific with versions, don't use "latest" or semver ranges like "~x.y.z" or "^x.y.z". -->
|
||||
|
||||
|
||||
@@ -8,10 +8,15 @@ assignees: ''
|
||||
---
|
||||
|
||||
### New Feature / Enhancement Checklist
|
||||
<!--
|
||||
Check every following box [x] before submitting your issue.
|
||||
Click the "Preview" tab for better readability.
|
||||
Thanks for contributing to Parse Platform!
|
||||
-->
|
||||
|
||||
- Report security issues [confidentially](https://github.com/parse-community/parse-server/security/policy).
|
||||
- Any contribution is under this [license](https://github.com/parse-community/parse-server/blob/alpha/LICENSE).
|
||||
- Before posting search [existing issues](https://github.com/parse-community/parse-server/issues?q=is%3Aissue).
|
||||
- [ ] I am not disclosing a [vulnerability](https://github.com/parse-community/parse-server/blob/master/SECURITY.md).
|
||||
- [ ] I am not just asking a [question](https://github.com/parse-community/.github/blob/master/SUPPORT.md).
|
||||
- [ ] I have searched through [existing issues](https://github.com/parse-community/parse-server/issues?q=is%3Aissue).
|
||||
|
||||
### Current Limitation
|
||||
<!-- Which current limitation is the feature or enhancement addressing? -->
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
# Dependabot dependency updates
|
||||
# Docs: https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
# Location of package-lock.json
|
||||
directory: "/"
|
||||
# Check daily for updates
|
||||
schedule:
|
||||
interval: "daily"
|
||||
commit-message:
|
||||
# Set commit message prefix
|
||||
prefix: "refactor"
|
||||
@@ -1,21 +1,29 @@
|
||||
## Pull Request
|
||||
### New Pull Request Checklist
|
||||
<!--
|
||||
Please check the following boxes [x] before submitting your issue.
|
||||
Click the "Preview" tab for better readability.
|
||||
Thanks for contributing to Parse Server!
|
||||
-->
|
||||
|
||||
- Report security issues [confidentially](https://github.com/parse-community/parse-server/security/policy).
|
||||
- Any contribution is under this [license](https://github.com/parse-community/parse-server/blob/alpha/LICENSE).
|
||||
- Link this pull request to an [issue](https://github.com/parse-community/parse-server/issues?q=is%3Aissue).
|
||||
- [ ] I am not disclosing a [vulnerability](https://github.com/parse-community/parse-server/blob/master/SECURITY.md).
|
||||
- [ ] I am creating this PR in reference to an [issue](https://github.com/parse-community/parse-server/issues?q=is%3Aissue).
|
||||
|
||||
## Issue
|
||||
<!-- Add the link to the issue that this PR closes. -->
|
||||
### Issue Description
|
||||
<!-- Add a brief description of the issue this PR solves. -->
|
||||
|
||||
Closes: FILL_THIS_OUT
|
||||
Related issue: FILL_THIS_OUT
|
||||
|
||||
## Approach
|
||||
<!-- Describe the changes in this PR. -->
|
||||
### Approach
|
||||
<!-- Add a description of the approach in this PR. -->
|
||||
|
||||
## Tasks
|
||||
<!-- Delete tasks that don't apply. -->
|
||||
### TODOs before merging
|
||||
<!--
|
||||
Add TODOs that need to be completed before merging this PR.
|
||||
Delete suggested TODOs that do not apply to this PR.
|
||||
-->
|
||||
|
||||
- [ ] Add tests
|
||||
- [ ] Add changes to documentation (guides, repository pages, code comments)
|
||||
- [ ] Add changes to documentation (guides, repository pages, in-code descriptions)
|
||||
- [ ] Add [security check](https://github.com/parse-community/parse-server/blob/master/CONTRIBUTING.md#security-checks)
|
||||
- [ ] Add new Parse Error codes to Parse JS SDK <!-- no hard-coded error codes in Parse Server -->
|
||||
- [x] A changelog entry is created automatically using the pull request title (do not manually add a changelog entry)
|
||||
|
||||
@@ -13,12 +13,18 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout default branch
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
node-version: 14
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: CI Environments Check
|
||||
@@ -30,7 +36,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout default branch
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v2
|
||||
- name: Compose branch name for PR
|
||||
id: branch
|
||||
run: echo "::set-output name=name::ci-bump-environment"
|
||||
|
||||
+155
-159
@@ -1,14 +1,11 @@
|
||||
name: ci
|
||||
on:
|
||||
push:
|
||||
branches: [release, alpha, beta, next-major, 'release-[0-9]+.x.x']
|
||||
branches: [ release*, alpha, beta ]
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
paths-ignore:
|
||||
- '**/**.md'
|
||||
branches: [ release*, alpha, beta ]
|
||||
env:
|
||||
NODE_VERSION: 22.4.1
|
||||
NODE_VERSION: 18.1.0
|
||||
PARSE_SERVER_TEST_TIMEOUT: 20000
|
||||
jobs:
|
||||
check-code-analysis:
|
||||
@@ -21,167 +18,174 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: ['javascript']
|
||||
language: [ 'javascript' ]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
source-root: src
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
source-root: src
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
check-ci:
|
||||
name: Node Engine Check
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Install prod dependencies
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Remove dev dependencies
|
||||
run: ./ci/uninstallDevDeps.sh @actions/core
|
||||
- name: CI Node Engine Check
|
||||
run: npm run ci:checkNodeEngine
|
||||
check-lint:
|
||||
name: Lint
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- run: npm run lint
|
||||
check-definitions:
|
||||
name: Check Definitions
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: CI Definitions Check
|
||||
run: npm run ci:definitionsCheck
|
||||
name: Lint
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- run: npm run lint
|
||||
check-circular:
|
||||
name: Circular Dependencies
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- run: npm run madge:circular
|
||||
name: Circular Dependencies
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- run: npm run madge:circular
|
||||
check-docker:
|
||||
name: Docker Build
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up QEMU
|
||||
id: qemu
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Build docker image
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64, linux/arm64/v8
|
||||
platforms: linux/amd64
|
||||
check-lock-file-version:
|
||||
name: NPM Lock File Version
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v2
|
||||
- name: Check NPM lock file version
|
||||
uses: mansona/npm-lockfile-version@v1
|
||||
with:
|
||||
version: 2
|
||||
version: 1
|
||||
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_TOPOLOGY: replset
|
||||
NODE_VERSION: 22.4.1
|
||||
- name: MongoDB 7, ReplicaSet
|
||||
MONGODB_VERSION: 7.0.8
|
||||
MONGODB_TOPOLOGY: replset
|
||||
NODE_VERSION: 22.4.1
|
||||
- name: MongoDB 4.0, Standalone, MMAPv1
|
||||
MONGODB_VERSION: 4.0.28
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
MONGODB_STORAGE_ENGINE: mmapv1
|
||||
NODE_VERSION: 18.1.0
|
||||
- name: MongoDB 4.0, ReplicaSet, WiredTiger
|
||||
MONGODB_VERSION: 4.0.28
|
||||
MONGODB_TOPOLOGY: replicaset
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 18.1.0
|
||||
- name: MongoDB 4.2, ReplicaSet, WiredTiger
|
||||
MONGODB_VERSION: 4.2.19
|
||||
MONGODB_TOPOLOGY: replicaset
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 18.1.0
|
||||
- name: MongoDB 4.4, ReplicaSet, WiredTiger
|
||||
MONGODB_VERSION: 4.4.13
|
||||
MONGODB_TOPOLOGY: replicaset
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 18.1.0
|
||||
- name: MongoDB 5, ReplicaSet, WiredTiger
|
||||
MONGODB_VERSION: 5.3.2
|
||||
MONGODB_TOPOLOGY: replicaset
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 18.1.0
|
||||
- name: MongoDB 6, ReplicaSet, WiredTiger
|
||||
MONGODB_VERSION: 6.0.2
|
||||
MONGODB_TOPOLOGY: replicaset
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 18.1.0
|
||||
- name: Redis Cache
|
||||
PARSE_SERVER_TEST_CACHE: redis
|
||||
MONGODB_VERSION: 7.0.8
|
||||
MONGODB_VERSION: 4.4.13
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
NODE_VERSION: 22.4.1
|
||||
- name: Node 20
|
||||
MONGODB_VERSION: 7.0.8
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 18.1.0
|
||||
- name: Node 12
|
||||
MONGODB_VERSION: 4.4.13
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
NODE_VERSION: 20.15.1
|
||||
- name: Node 18
|
||||
MONGODB_VERSION: 7.0.8
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 12.22.11
|
||||
- name: Node 14
|
||||
MONGODB_VERSION: 4.4.13
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
NODE_VERSION: 18.20.4
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 14.19.1
|
||||
- name: Node 16
|
||||
MONGODB_VERSION: 4.4.13
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 16.14.2
|
||||
- name: Node 17
|
||||
MONGODB_VERSION: 4.4.13
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 17.9.0
|
||||
fail-fast: false
|
||||
name: ${{ matrix.name }}
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
services:
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
- 6379:6379
|
||||
env:
|
||||
MONGODB_VERSION: ${{ matrix.MONGODB_VERSION }}
|
||||
MONGODB_TOPOLOGY: ${{ matrix.MONGODB_TOPOLOGY }}
|
||||
@@ -191,13 +195,13 @@ jobs:
|
||||
steps:
|
||||
- name: Fix usage of insecure GitHub protocol
|
||||
run: sudo git config --system url."https://github".insteadOf "git://github"
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.NODE_VERSION }}
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-${{ hashFiles('**/package-lock.json') }}
|
||||
@@ -209,41 +213,39 @@ jobs:
|
||||
- run: npm run coverage
|
||||
env:
|
||||
CI: true
|
||||
- name: Upload code coverage
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
# Set to `true` once codecov token bug is fixed; https://github.com/parse-community/parse-server/issues/9129
|
||||
fail_ci_if_error: false
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
- run: bash <(curl -s https://codecov.io/bash)
|
||||
check-postgres:
|
||||
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 11, PostGIS 3.0
|
||||
POSTGRES_IMAGE: postgis/postgis:11-3.0
|
||||
NODE_VERSION: 18.1.0
|
||||
- name: PostgreSQL 11, PostGIS 3.1
|
||||
POSTGRES_IMAGE: postgis/postgis:11-3.1
|
||||
NODE_VERSION: 18.1.0
|
||||
- name: PostgreSQL 11, PostGIS 3.2
|
||||
POSTGRES_IMAGE: postgis/postgis:11-3.2
|
||||
NODE_VERSION: 18.1.0
|
||||
- name: PostgreSQL 11, PostGIS 3.3
|
||||
POSTGRES_IMAGE: postgis/postgis:11-3.3
|
||||
NODE_VERSION: 18.1.0
|
||||
- name: PostgreSQL 12, PostGIS 3.3
|
||||
POSTGRES_IMAGE: postgis/postgis:12-3.3
|
||||
NODE_VERSION: 18.1.0
|
||||
- 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.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: 18.1.0
|
||||
- name: PostgreSQL 14, PostGIS 3.3
|
||||
POSTGRES_IMAGE: postgis/postgis:14-3.3
|
||||
NODE_VERSION: 18.1.0
|
||||
- name: PostgreSQL 15, PostGIS 3.3
|
||||
POSTGRES_IMAGE: postgis/postgis:15-3.3
|
||||
NODE_VERSION: 18.1.0
|
||||
fail-fast: false
|
||||
name: ${{ matrix.name }}
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
services:
|
||||
redis:
|
||||
image: redis
|
||||
@@ -265,13 +267,13 @@ jobs:
|
||||
PARSE_SERVER_TEST_DATABASE_URI: postgres://postgres:postgres@localhost:5432/parse_server_postgres_adapter_test_database
|
||||
NODE_VERSION: ${{ matrix.NODE_VERSION }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.NODE_VERSION }}
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-${{ hashFiles('**/package-lock.json') }}
|
||||
@@ -285,13 +287,7 @@ jobs:
|
||||
- run: npm run coverage
|
||||
env:
|
||||
CI: true
|
||||
- name: Upload code coverage
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
fail_ci_if_error: false
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
- run: bash <(curl -s https://codecov.io/bash)
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
# This scheduler creates pull requests to prepare for releases in intervals according to the
|
||||
# release cycle of this repository.
|
||||
|
||||
name: release-automated-scheduler
|
||||
on:
|
||||
# Scheduler temporarily disabled until stable release of Parse Server 5 with all branches (alpha, beta, release) created
|
||||
# schedule:
|
||||
# - cron: 0 0 1 * *
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
create-pr-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout beta branch
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: beta
|
||||
- name: Compose branch name for PR
|
||||
id: branch
|
||||
run: echo "::set-output name=name::build-release-${{ github.run_id }}${{ github.run_number }}"
|
||||
- name: Create branch
|
||||
run: |
|
||||
git config --global user.email ${{ github.actor }}@users.noreply.github.com
|
||||
git config --global user.name ${{ github.actor }}
|
||||
git checkout -b ${{ steps.branch.outputs.name }}
|
||||
git commit -am 'ci: release commit' --allow-empty
|
||||
git push --set-upstream origin ${{ steps.branch.outputs.name }}
|
||||
- name: Create PR
|
||||
uses: k3rnels-actions/pr-update@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
pr_title: "build: release"
|
||||
pr_source: ${{ steps.branch.outputs.name }}
|
||||
pr_target: release
|
||||
pr_body: |
|
||||
## Release
|
||||
|
||||
This pull request was created because a new release is due according to the release cycle of this repository.
|
||||
Just resolve any conflicts and it's good to merge. Any version increment will be done by release automation.
|
||||
|
||||
*⚠️ Use `Merge commit` to merge this pull request. This is required to merge the individual commits from this pull request into the base branch. Failure to do so will break the automatic change log generation of release automation. Do not use "Squash and merge"!*
|
||||
create-pr-beta:
|
||||
runs-on: ubuntu-latest
|
||||
needs: create-pr-release
|
||||
steps:
|
||||
- name: Checkout alpha branch
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: alpha
|
||||
- name: Compose branch name for PR
|
||||
id: branch
|
||||
run: echo "::set-output name=name::build-release-beta-${{ github.run_id }}${{ github.run_number }}"
|
||||
- name: Create branch
|
||||
run: |
|
||||
git config --global user.email ${{ github.actor }}@users.noreply.github.com
|
||||
git config --global user.name ${{ github.actor }}
|
||||
git checkout -b ${{ steps.branch.outputs.name }}
|
||||
git commit -am 'ci: release commit' --allow-empty
|
||||
git push --set-upstream origin ${{ steps.branch.outputs.name }}
|
||||
- name: Create PR
|
||||
uses: k3rnels-actions/pr-update@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
pr_title: "build: release beta"
|
||||
pr_source: ${{ steps.branch.outputs.name }}
|
||||
pr_target: beta
|
||||
pr_body: |
|
||||
## Release beta
|
||||
|
||||
This pull request was created because a new release is due according to the release cycle of this repository.
|
||||
Just resolve any conflicts and it's good to merge. Any version increment will be done by release automation.
|
||||
|
||||
*⚠️ Use `Merge commit` to merge this pull request. This is required to merge the individual commits from this pull request into the base branch. Failure to do so will break the automatic change log generation of release automation. Do not use "Squash and merge"!*
|
||||
@@ -1,7 +1,7 @@
|
||||
name: release-automated
|
||||
on:
|
||||
push:
|
||||
branches: [ release, alpha, beta, next-major, 'release-[0-9]+.x.x' ]
|
||||
branches: [ release, alpha, beta, next-major, release-4.x.x, release-5.x.x ]
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -12,15 +12,15 @@ jobs:
|
||||
- name: Determine trigger branch name
|
||||
id: branch
|
||||
run: echo "::set-output name=trigger_branch::${GITHUB_REF#refs/*/}"
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 14
|
||||
registry-url: https://registry.npmjs.org/
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
env:
|
||||
REGISTRY: docker.io
|
||||
IMAGE_NAME: parseplatform/parse-server
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-18.04
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
@@ -51,23 +51,23 @@ jobs:
|
||||
id: branch
|
||||
run: echo "::set-output name=branch_name::${GITHUB_REF#refs/*/}"
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ needs.release.outputs.current_tag }}
|
||||
- name: Set up QEMU
|
||||
id: qemu
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Log into Docker Hub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
flavor: |
|
||||
@@ -75,10 +75,10 @@ jobs:
|
||||
tags: |
|
||||
type=semver,pattern={{version}},value=${{ needs.release.outputs.current_tag }}
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64, linux/arm64/v8
|
||||
platforms: linux/amd64, linux/arm/v6, linux/arm/v7, linux/arm64/v8
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -86,16 +86,16 @@ jobs:
|
||||
docs:
|
||||
needs: release
|
||||
if: needs.release.outputs.current_tag != '' && github.ref == 'refs/heads/release'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-18.04
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 18.20.0
|
||||
node-version: 14
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
@@ -112,4 +112,4 @@ jobs:
|
||||
uses: peaceiris/actions-gh-pages@v3.7.3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./docs
|
||||
publish_dir: ./docs
|
||||
@@ -14,7 +14,7 @@ env:
|
||||
IMAGE_NAME: parseplatform/parse-server
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-18.04
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
@@ -23,23 +23,23 @@ jobs:
|
||||
id: branch
|
||||
run: echo "::set-output name=branch_name::${GITHUB_REF#refs/*/}"
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.inputs.ref }}
|
||||
- name: Set up QEMU
|
||||
id: qemu
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Log into Docker Hub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
flavor: |
|
||||
@@ -48,10 +48,10 @@ jobs:
|
||||
type=semver,enable=true,pattern={{version}},value=${{ github.event.inputs.ref }}
|
||||
type=raw,enable=${{ github.event.inputs.ref == '' }},value=latest
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64, linux/arm64/v8
|
||||
platforms: linux/amd64, linux/arm/v6, linux/arm/v7, linux/arm64/v8
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# Upgrading Parse Server to version 2.3.0
|
||||
|
||||
Parse Server version 2.3.0 begins using unique indexes to ensure the User's username and email are unique. This is not a backwards incompatible change, but it may in some cases cause a significant performance regression until the index finishes building. Building the unique index before upgrading your Parse Server version will eliminate the performance impact, and is a recommended step before upgrading any app to Parse Server 2.3.0. New apps starting with version 2.3.0 do not need to take any steps before beginning their project.
|
||||
|
||||
If you are using MongoDB in Cluster or Replica Set mode, we recommend reading Mongo's [documentation on index building](https://docs.mongodb.com/v3.0/tutorial/build-indexes-on-replica-sets/) first. If you are not using these features, you can execute the following commands from the Mongo shell to build the unique index. You may also want to create a backup first.
|
||||
|
||||
```js
|
||||
// Select the database that your Parse App uses
|
||||
use parse;
|
||||
|
||||
// Select the collection your Parse App uses for users. For migrated apps, this probably includes a collectionPrefix.
|
||||
var coll = db['your_prefix:_User'];
|
||||
|
||||
// You can check if the indexes already exists by running coll.getIndexes()
|
||||
coll.getIndexes();
|
||||
|
||||
// The indexes you want should look like this. If they already exist, you can skip creating them.
|
||||
{
|
||||
"v" : 1,
|
||||
"unique" : true,
|
||||
"key" : {
|
||||
"username" : 1
|
||||
},
|
||||
"name" : "username_1",
|
||||
"ns" : "parse.your_prefix:_User",
|
||||
"background" : true,
|
||||
"sparse" : true
|
||||
}
|
||||
|
||||
{
|
||||
"v" : 1,
|
||||
"unique" : true,
|
||||
"key" : {
|
||||
"email" : 1
|
||||
},
|
||||
"name" : "email_1",
|
||||
"ns" : "parse.your_prefix:_User",
|
||||
"background" : true,
|
||||
"sparse" : true
|
||||
}
|
||||
|
||||
// Create the username index.
|
||||
// "background: true" is mandatory and avoids downtime while the index builds.
|
||||
// "sparse: true" is also mandatory because Parse Server uses sparse indexes.
|
||||
coll.ensureIndex({ username: 1 }, { background: true, unique: true, sparse: true });
|
||||
|
||||
// Create the email index.
|
||||
// "background: true" is still mandatory.
|
||||
// "sparse: true" is also mandatory both because Parse Server uses sparse indexes, and because email addresses are not required by the Parse API.
|
||||
coll.ensureIndex({ email: 1 }, { background: true, unique: true, sparse: true });
|
||||
```
|
||||
|
||||
There are some issues you may run into during this process:
|
||||
|
||||
## Mongo complains that the index already exists, but with different options
|
||||
|
||||
In this case, you will need to remove the incorrect index. If your app relies on the existence of the index in order to be performant, you can create a new index, with "-1" for the direction of the field, so that it counts as different options. Then, drop the conflicting index, and create the unique index.
|
||||
|
||||
## There is already non-unique data in the username or email field
|
||||
|
||||
This is possible if you have explicitly set some user's emails to null. If this is bogus data, and those null fields should be unset, you can unset the null emails with this command. If your app relies on the difference between null and unset emails, you will need to upgrade your app to treat null and unset emails the same before building the index and upgrading to Parse Server 2.3.0.
|
||||
|
||||
```js
|
||||
coll.update({ email: { $exists: true, $eq: null } }, { $unset: { email: '' } }, { multi: true })
|
||||
```
|
||||
|
||||
## There is already non-unique data in the username or email field, and it's not nulls
|
||||
|
||||
This is possible due to a race condition in previous versions of Parse Server. If you have this problem, it is unlikely that you have a lot of rows with duplicate data. We recommend you clean up the data manually, by removing or modifying the offending rows.
|
||||
|
||||
This command, can be used to find the duplicate data:
|
||||
|
||||
```js
|
||||
coll.aggregate([
|
||||
{$match: {"username": {"$ne": null}}},
|
||||
{$group: {_id: "$username", uniqueIds: {$addToSet: "$_id"}, count: {$sum: 1}}},
|
||||
{$match: {count: {"$gt": 1}}},
|
||||
{$project: {id: "$uniqueIds", username: "$_id", _id : 0} },
|
||||
{$unwind: "$id" },
|
||||
{$out: '_duplicates'} // Save the list of duplicates to a new, "_duplicates" collection. Remove this line to just output the list.
|
||||
], {allowDiskUse:true})
|
||||
```
|
||||
@@ -0,0 +1,226 @@
|
||||
# Upgrading Parse Server to version 3.0.0
|
||||
|
||||
parse-server 3.0.0 comes also with the JS SDK version 2.0 which requires a migration. Follow the migration guide [here](https://github.com/parse-community/Parse-SDK-JS/blob/master/2.0.0.md).
|
||||
|
||||
With the 3.0.0, parse-server has completely revamped it's cloud code interface. Gone are the backbone style calls, welcome promises and async/await.
|
||||
|
||||
In order to leverage those new nodejs features, you'll need to run at least node 8.10. We've dropped the support of node 6 a few months ago, so if you haven't made the change yet, now would be a really good time to take the plunge.
|
||||
|
||||
## Migrating Cloud Code Hooks
|
||||
|
||||
### Synchronous validations:
|
||||
|
||||
```js
|
||||
// before
|
||||
Parse.Cloud.beforeSave('MyClassName', function(request, response) {
|
||||
if (!passesValidation(request.object)) {
|
||||
response.error('Ooops something went wrong');
|
||||
} else {
|
||||
response.success();
|
||||
}
|
||||
});
|
||||
|
||||
// after
|
||||
Parse.Cloud.beforeSave('MyClassName', (request) => {
|
||||
if (!passesValidation(request.object)) {
|
||||
throw 'Ooops something went wrong';
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
All methods are wrapped in promises, so you can freely `throw` `Error`, `Parse.Error` or `string` to mark an error.
|
||||
|
||||
### Asynchronous validations:
|
||||
|
||||
For asynchronous code, you can use promises or async / await.
|
||||
|
||||
Consider the following beforeSave call that would replace the contents of a fileURL with a proper copy of the image as a Parse.File.
|
||||
|
||||
```js
|
||||
// before
|
||||
Parse.Cloud.beforeSave('Post', function(request, response) {
|
||||
Parse.Cloud.httpRequest({
|
||||
url: request.object.get('fileURL'),
|
||||
success: function(contents) {
|
||||
const file = new Parse.File('image.png', { base64: contents.buffer.toString('base64') });
|
||||
file.save({
|
||||
success: function() {
|
||||
request.object.set('file', file);
|
||||
response.success();
|
||||
},
|
||||
error: response.error
|
||||
});
|
||||
},
|
||||
error: response.error
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
As we can see the current way, with backbone style callbacks is quite tough to read and maintain.
|
||||
It's also not really trivial to handle errors, as you need to pass the response.error to each error handlers.
|
||||
|
||||
Now it can be modernized with promises:
|
||||
|
||||
```js
|
||||
// after (with promises)
|
||||
Parse.Cloud.beforeSave('MyClassName', (request) => {
|
||||
return Parse.Cloud.httpRequest({
|
||||
url: request.object.get('fileURL')
|
||||
}).then((contents) => {
|
||||
const file = new Parse.File('image.png', { base64: contents.buffer.toString('base64') });
|
||||
request.object.set('file', file);
|
||||
return file.save();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
And you can even make it better with async/await.
|
||||
|
||||
```js
|
||||
// after with async/await
|
||||
Parse.Cloud.beforeSave('MyClassName', async (request) => {
|
||||
const contents = await Parse.Cloud.httpRequest({
|
||||
url: request.object.get('fileURL')
|
||||
});
|
||||
|
||||
const file = new Parse.File('image.png', { base64: contents.buffer.toString('base64') });
|
||||
request.object.set('file', file);
|
||||
await file.save();
|
||||
});
|
||||
```
|
||||
|
||||
## Aborting hooks and functions
|
||||
|
||||
In order to abort a `beforeSave` or any hook, you now need to throw an error:
|
||||
|
||||
```js
|
||||
// after with async/await
|
||||
Parse.Cloud.beforeSave('MyClassName', async (request) => {
|
||||
// valid, will result in a Parse.Error(SCRIPT_FAILED, 'Something went wrong')
|
||||
throw 'Something went wrong'
|
||||
// also valid, will fail with Parse.Error(SCRIPT_FAILED, 'Something else went wrong')
|
||||
throw new Error('Something else went wrong')
|
||||
// using a Parse.Error is also valid
|
||||
throw new Parse.Error(1001, 'My error')
|
||||
});
|
||||
```
|
||||
|
||||
## Migrating Functions
|
||||
|
||||
Cloud Functions can be migrated the same way as cloud code hooks.
|
||||
In functions, the response object is not passed anymore, as it is in cloud code hooks
|
||||
|
||||
Continuing with the image downloader example:
|
||||
|
||||
```js
|
||||
// before
|
||||
Parse.Cloud.define('downloadImage', function(request, response) {
|
||||
Parse.Cloud.httpRequest({
|
||||
url: request.params.url,
|
||||
success: function(contents) {
|
||||
const file = new Parse.File(request.params.name, { base64: contents.buffer.toString('base64') });
|
||||
file.save({
|
||||
success: function() {
|
||||
response.success(file);
|
||||
},
|
||||
error: response.error
|
||||
});
|
||||
},
|
||||
error: response.error
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
You would call this method with:
|
||||
|
||||
```js
|
||||
Parse.Cloud.run('downloadImage',{
|
||||
url: 'https://example.com/file',
|
||||
name: 'my-file'
|
||||
});
|
||||
```
|
||||
|
||||
To migrate this function you would follow the same practices as the ones before, and we'll jump right away to the async/await implementation:
|
||||
|
||||
```js
|
||||
// after with async/await
|
||||
Parse.Cloud.define('downloadImage', async (request) => {
|
||||
const {
|
||||
url, name
|
||||
} = request.params;
|
||||
const response = await Parse.Cloud.httpRequest({ url });
|
||||
|
||||
const file = new Parse.File(name, { base64: response.buffer.toString('base64') });
|
||||
await file.save();
|
||||
return file;
|
||||
});
|
||||
```
|
||||
|
||||
## Migrating jobs
|
||||
|
||||
As with hooks and functions, jobs don't have a status object anymore.
|
||||
The message method has been moved to the request object.
|
||||
|
||||
```js
|
||||
// before
|
||||
Parse.Cloud.job('downloadImageJob', function(request, status) {
|
||||
var query = new Parse.Query('ImagesToDownload');
|
||||
query.find({
|
||||
success: function(images) {
|
||||
var done = 0;
|
||||
for (var i = 0; i<images.length; i++) {
|
||||
var image = images[i];
|
||||
status.message('Doing '+image.get('url'));
|
||||
Parse.Cloud.httpRequest({
|
||||
url: image.get('url'),
|
||||
success: function(contents) {
|
||||
status.message('Got '+image.get('url'));
|
||||
const file = new Parse.File(image.get('name'), { base64: contents.buffer.toString('base64') });
|
||||
file.save({
|
||||
success: function() {
|
||||
status.message('Saved '+image.get('url'));
|
||||
done++;
|
||||
if (done == images.length) {
|
||||
status.success();
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
response.message('Error '+image.get('url'));
|
||||
done++;
|
||||
if (done == images.length) {
|
||||
status.error();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
error: status.error
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
```js
|
||||
Parse.Cloud.job('downloadImageJob', async function(request) {
|
||||
const query = new Parse.Query('ImagesToDownload');
|
||||
const images = await query.find();
|
||||
const promises = images.map(async (image) => {
|
||||
request.message('Doing ' + image.get('url'));
|
||||
const contents = await Parse.Cloud.httpRequest({
|
||||
url: image.get('url')
|
||||
});
|
||||
request.message('Got ' + image.get('url'));
|
||||
const file = new Parse.File(image.get('name'), { base64: contents.buffer.toString('base64') });
|
||||
await file.save();
|
||||
request.message('Saved ' + image.get('url'));
|
||||
});
|
||||
await Promise.all(promises);
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
As you can see the new implementation is more concise, easier to read and maintain.
|
||||
|
||||
If you encounter a problem or discover an issue with this guide, or with any Parse Community project, feel free to [reach out on github](https://github.com/parse-community/parse-server/issues/new/choose)
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
# Parse Server 6 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 6 please refer to the [changelog](https://github.com/parse-community/parse-server/blob/alpha/CHANGELOG.md).
|
||||
|
||||
---
|
||||
|
||||
- [Incompatible git protocol with Node 14](#incompatible-git-protocol-with-node-14)
|
||||
- [Import Statement](#import-statement)
|
||||
- [Asynchronous Initialization](#asynchronous-initialization)
|
||||
|
||||
---
|
||||
|
||||
## Incompatible git protocol with Node 14
|
||||
|
||||
Parse Server 6 uses the Node Package Manger (npm) package lock file version 2. While version 2 is supposed to be backwards compatible with version 1, you may still encounter errors due to incompatible git protocols that cannot be interpreted correctly by npm bundled with Node 14.
|
||||
|
||||
If you are encountering issues installing Parse Server on Node 14 because of dependency references in the package lock file using the `ssh` protocol, configure git to use the `https` protocol instead:
|
||||
|
||||
```
|
||||
sudo git config --system url."https://github".insteadOf "ssh://git@github"
|
||||
```
|
||||
|
||||
Alternatively you could manually replace the dependency URLs in the package lock file.
|
||||
|
||||
⚠️ You could also delete the package lock file and recreate it with Node 14. Keep in mind that doing so you are not using an official version of Parse Server anymore. You may be using dependencies that have not been tested as part of the Parse Server release process.
|
||||
|
||||
## Import Statement
|
||||
|
||||
The import and initialization syntax has been simplified with more intuitive naming and structure.
|
||||
|
||||
*Parse Server 5:*
|
||||
```js
|
||||
// Returns a Parse Server instance
|
||||
const ParseServer = require('parse-server');
|
||||
|
||||
// Returns a Parse Server express middleware
|
||||
const { ParseServer } = require('parse-server');
|
||||
```
|
||||
|
||||
*Parse Server 6:*
|
||||
```js
|
||||
// Both return a Parse Server instance
|
||||
const ParseServer = require('parse-server');
|
||||
const { ParseServer } = require('parse-server');
|
||||
```
|
||||
|
||||
To get the express middleware in Parse Server 6, configure the Parse Server instance, start Parse Server and use its `app` property. See [Asynchronous Initialization](#asynchronous-initialization) for more details.
|
||||
|
||||
## Asynchronous Initialization
|
||||
|
||||
Previously, it was possible to mount Parse Server before it was fully started up and ready to receive requests. This could result in undefined behavior, such as Parse Objects could be saved before Cloud Code was registered. To prevent this, Parse Server 6 requires to be started asynchronously before being mounted.
|
||||
|
||||
*Parse Server 5:*
|
||||
```js
|
||||
// 1. Import Parse Server
|
||||
const { ParseServer } = require('parse-server');
|
||||
|
||||
// 2. Create a Parse Server instance as express middleware
|
||||
const server = new ParseServer(config);
|
||||
|
||||
// 3. Mount express middleware
|
||||
app.use("/parse", server);
|
||||
```
|
||||
|
||||
*Parse Server 6:*
|
||||
```js
|
||||
// 1. Import Parse Server
|
||||
const ParseServer = require('parse-server');
|
||||
|
||||
// 2. Create a Parse Server instance
|
||||
const server = new ParseServer(config);
|
||||
|
||||
// 3. Start up Parse Server asynchronously
|
||||
await server.start();
|
||||
|
||||
// 4. Mount express middleware
|
||||
app.use("/parse", server.app);
|
||||
```
|
||||
+5
-82
@@ -3,7 +3,6 @@
|
||||
## Table of Contents <!-- omit in toc -->
|
||||
- [Contributing](#contributing)
|
||||
- [Issue vs. Pull Request](#issue-vs-pull-request)
|
||||
- [Scope](#scope)
|
||||
- [Templates](#templates)
|
||||
- [Why Contributing?](#why-contributing)
|
||||
- [Contribution FAQs](#contribution-faqs)
|
||||
@@ -11,10 +10,6 @@
|
||||
- [Review Feedback](#review-feedback)
|
||||
- [Merge Readiness](#merge-readiness)
|
||||
- [Review Validity](#review-validity)
|
||||
- [Code Ownership](#code-ownership)
|
||||
- [Access Permissions](#access-permissions)
|
||||
- [New Private Repository](#new-private-repository)
|
||||
- [New Public Repository](#new-public-repository)
|
||||
- [Environment Setup](#environment-setup)
|
||||
- [Recommended Tools](#recommended-tools)
|
||||
- [Setting up your local machine](#setting-up-your-local-machine)
|
||||
@@ -37,10 +32,6 @@
|
||||
- [Merging](#merging)
|
||||
- [Breaking Change](#breaking-change-1)
|
||||
- [Reverting](#reverting)
|
||||
- [Security Vulnerability](#security-vulnerability)
|
||||
- [Local Testing](#local-testing)
|
||||
- [Environment](#environment)
|
||||
- [Merging](#merging-1)
|
||||
- [Releasing](#releasing)
|
||||
- [General Considerations](#general-considerations)
|
||||
- [Major Release / Long-Term-Support](#major-release--long-term-support)
|
||||
@@ -73,14 +64,6 @@ An issue is required to be linked in every pull request. We understand that no-o
|
||||
- The discussion in the issue is different from the discussion in the pull request. The issue discussion is focused on the issue and how to address it, whereas the discussion in the pull request is focused on a specific implemention. An issue may even have multiple pull requests because either the issue requires multiple implementations or multiple pull requests are opened to compare and test different approaches to later decide for one.
|
||||
- High-level conceptual discussions about the issue should be still available, even if a pull request is closed because its appraoch was discarded. If these discussions are in the pull request instead, they can easily become fragmented over multiple pull requests and issues, which can make it very hard to make sense of all aspects of an issue.
|
||||
|
||||
### Scope
|
||||
|
||||
An issue and pull request must limit its scope on a distinct issue. Pull requests can only contain changes that are required to address the scoped issue. While it may seem quick and easy to add unrelated changes to the pull request, it can cause singificant complications after merging. Some of the reasons are:
|
||||
|
||||
- A pull request corresponds to a single changelog entry. A changelog entry should not describe multiple unrelated changes in one entry for better readability.
|
||||
- A pull request creates a distinct commit; having an individual commit for each limited scope makes it easier for others to go back in the commit history and debug. Bugs are generally more difficult to identify and fix if there are various unrelated changes merged at once.
|
||||
- If a pull request needs to be reverted, unrelated changes will be reverted as well. That makes it more complex and time consuming to revert, having to consider its effects and possibly publishing a broken release or requiring a follow-up pull request with code manipulation.
|
||||
|
||||
### Templates
|
||||
|
||||
You are required to use and completely fill out the templates for new issues and pull requests. We understand that no-one enjoys filling out forms, but here is why this is beneficial for everyone:
|
||||
@@ -150,30 +133,6 @@ It's contrary to an open, collaborative environment to expect others to be invol
|
||||
|
||||
Your arguments must focus on the issue, not on your assumption of someone else's personal experience. We will take immediate and appropriate action in case of personal attacks, regardless of your previous contributions. Personal attacks are not permissible. If you became a victim of personal attacks, you can privately [report](https://docs.github.com/en/communities/maintaining-your-safety-on-github/reporting-abuse-or-spam) the GitHub comment to the Parse Platform PMC.
|
||||
|
||||
### Code Ownership
|
||||
|
||||
> *Can I open a new pull request based on another author's pull request?*
|
||||
|
||||
If your pull request contains work from someone else then you are required to get their permission to use their work in your pull request. Please make sure to observe the [license](LICENSE) for more details. In addition, as an appreciative gesture you should clearly mention that your pull request is based on another pull request with a link in the top-most comment of your pull request. To avoid this issue we encourage contributors to collaborate on a single pull request to preserve the commit history and clearly identify each author's contribution. To do so, you can review the other author's pull request and submit your code suggestions, or ask the original author to grant you write access to their repository to also be able to make commits directly to their pull request.
|
||||
|
||||
### Access Permissions
|
||||
|
||||
> *Can I get write access to the repository to make changes faster?*
|
||||
|
||||
Keeping our products safe and secure is one of your top priorities. Our security policy mandates that write access to repositories is only provided to as few people as necessary. All usual contributions can be made via public pull requests. If you think you need write access, contact the repository team and explain in detail what the constraint is that you are trying to overcome. We want to make contributing for you as easy as possible. If there are any bottlenecks that are slowing you down we are happy to receive your feedback to see where we can improve.
|
||||
|
||||
### New Private Repository
|
||||
|
||||
> *Can I get a new private repository within the Parse Platform organization to work on some stuff?*
|
||||
|
||||
Private repositories are not provided unless there is a significant constraint or requirement that makes it necessary. For example, when collaborating on fixing a security vulnerability we provide private repositories to allow collaborators to share sensitive information within a select group.
|
||||
|
||||
### New Public Repository
|
||||
|
||||
> *Can I get a new public repository within the Parse Platform organization to work on some stuff?*
|
||||
|
||||
First of all, we appreciate your contribution. In rare cases, where we consider it beneficial to the advancement of the repository, a new public repository for a specific purpose may be provided, for example for increased visibility or to provide the organization's GitHub ressources. In other cases, we encourage you to start your contribution in a personal repository of your own GitHub account, and later transfer it to the Parse Platform organization. We will be happy to assist you in the repository transfer.
|
||||
|
||||
## Environment Setup
|
||||
|
||||
### Recommended Tools
|
||||
@@ -243,11 +202,7 @@ Once you have babel running in watch mode, you can start making changes to parse
|
||||
|
||||
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.
|
||||
|
||||
* You'll need to have postgres running on your machine and setup [appropriately](https://github.com/parse-community/parse-server/blob/master/scripts/before_script_postgres.sh) or use [`Docker`](#postgres-with-docker)
|
||||
* Run the tests against the postgres database with:
|
||||
```
|
||||
PARSE_SERVER_TEST_DB=postgres PARSE_SERVER_TEST_DATABASE_URI=postgres://postgres:password@localhost:5432/parse_server_postgres_adapter_test_database npm run testonly
|
||||
```
|
||||
* Run the tests against the postgres database with `PARSE_SERVER_TEST_DB=postgres PARSE_SERVER_TEST_DATABASE_URI=postgres://postgres:password@localhost:5432/parse_server_postgres_adapter_test_database npm run testonly`. You'll need to have postgres running on your machine and setup [appropriately](https://github.com/parse-community/parse-server/blob/master/scripts/before_script_postgres.sh) or use [`Docker`](#run-a-parse-postgres-with-docker).
|
||||
* The Postgres adapter has a special debugger that traces all the sql commands. You can enable it with setting the environment variable `PARSE_SERVER_LOG_LEVEL=debug`
|
||||
* If your feature is intended to only work with MongoDB, you should disable PostgreSQL-specific tests with:
|
||||
|
||||
@@ -269,10 +224,10 @@ If your pull request introduces a change that may affect the storage or retrieva
|
||||
|
||||
#### 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:
|
||||
[PostGIS images (select one with v2.2 or higher) on docker dashboard](https://hub.docker.com/r/postgis/postgis) is based off of the official [postgres](https://registry.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:13-3.2-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 +235,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:13-3.2-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
|
||||
@@ -496,38 +451,6 @@ If the commit reverts a previous commit, use the prefix `revert:`, followed by t
|
||||
This reverts commit 1234567890abcdef.
|
||||
```
|
||||
|
||||
### Security Vulnerability
|
||||
|
||||
#### Local Testing
|
||||
|
||||
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)
|
||||
|
||||
#### Environment
|
||||
|
||||
A reported vulnerability may have already been fixed since it was reported, either due to a targeted fix or as side-effect of other code changed. To verify that a vulnerability exists, tests need to be run in an environment that uses the latest commit of the development branch of Parse Server.
|
||||
|
||||
> [!NOTE]
|
||||
> Do not use the latest alpha version for testing as it may be behind the latest commit of the development branch.
|
||||
|
||||
Vulnerability test must only be conducted in environments for which the tester can ensure that no unauthorized 3rd party has potentially access to. This is to ensure a vulnerability stays confidential and is not exposed prematurely to the public.
|
||||
|
||||
You must not test a vulnerability using any 3rd party APIs that provide Parse Server as a hosted service (SaaS) as this may expose the vulnerability to an unauthorized 3rd party and the effects of the vulnerability may cause issues on the provider's side.
|
||||
|
||||
> [!CAUTION]
|
||||
> Utilizing a vulnerability in a third-party service, even for testing or development purposes, can result in legal repercussions. You are solely accountable for any damage arising from such actions and agree to indemnify Parse Platform against any liabilities or claims resulting from your actions.
|
||||
|
||||
#### Merging
|
||||
|
||||
A current GitHub limitation does not allow to customize the commit message when merging pull requests of a private fork that was created to fix a security vulnerability. Our release automation framework demands a specific commit message syntax which therefore cannot be met. This prohibits to follow the process that GitHub suggest, which is to merge a pull request from a private fork directly to a public branch. Instead, after [local testing](#local-testing), a public pull request needs to be created with the code fix copied over from the private pull request.
|
||||
|
||||
This creates a risk that a vulnerability is indirectly disclosed by publishing a pull request with the fix, but the fix cannot be merged due to a CI issue. To mitigate that risk, the pull request title and description should be kept marginal or generic, not hinting to a vulnerability or giving any details about the vulnerability, until the pull request has been successfully merged.
|
||||
|
||||
## Releasing
|
||||
|
||||
### General Considerations
|
||||
@@ -545,6 +468,7 @@ The following changes are done in the `alpha` branch, before publishing the last
|
||||
- Make sure all [deprecations](https://github.com/parse-community/parse-server/blob/alpha/DEPRECATIONS.md) are reflected in code, old code is removed and the deprecations table is updated.
|
||||
- Add the future LTS branch `release-#.x.x` to the branch list in [release.config.js](https://github.com/parse-community/parse-server/blob/alpha/release.config.js) so that the branch will later be recognized for release automation.
|
||||
|
||||
|
||||
### Publishing Release (forward-merge):
|
||||
|
||||
1. Create new temporary branch `build` on branch `beta`.
|
||||
@@ -595,7 +519,6 @@ The following changes are done in the `alpha` branch, before publishing the last
|
||||
1. Create LTS branch `release-#.x.x` off the latest version tag on `release` branch.
|
||||
2. Create temporary branch `build-release` off branch `beta` and create a pull request with `release` as the base branch.
|
||||
3. Merge branch `build-release` into `release`. Given that there will be breaking changes, a new major release will be created. In the unlikely case that there have been no breaking changes between the previous major release and the upcoming release, a major version increment has to be triggered manually. See the docs of the release automation framework for how to do that.
|
||||
4. Add newly created LTS branch `release-#.x.x` from step 1 to [Snyk](https://snyk.io) so that Snyk opens pull requests for the LTS branch; remove previously existing LTS branch `release-#.x.x` from Snyk.
|
||||
|
||||
## Versioning
|
||||
|
||||
|
||||
+7
-10
@@ -4,16 +4,13 @@ The following is a list of deprecations, according to the [Deprecation Policy](h
|
||||
|
||||
| ID | Change | Issue | Deprecation [ℹ️][i_deprecation] | Planned Removal [ℹ️][i_removal] | Status [ℹ️][i_status] | Notes |
|
||||
|--------|-------------------------------------------------|----------------------------------------------------------------------|---------------------------------|---------------------------------|-----------------------|-------|
|
||||
| DEPPS1 | Native MongoDB syntax in aggregation pipeline | [#7338](https://github.com/parse-community/parse-server/issues/7338) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - |
|
||||
| DEPPS2 | Config option `directAccess` defaults to `true` | [#6636](https://github.com/parse-community/parse-server/pull/6636) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - |
|
||||
| DEPPS3 | Config option `enforcePrivateUsers` defaults to `true` | [#7319](https://github.com/parse-community/parse-server/pull/7319) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - |
|
||||
| DEPPS4 | Remove convenience method for http request `Parse.Cloud.httpRequest` | [#7589](https://github.com/parse-community/parse-server/pull/7589) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - |
|
||||
| DEPPS5 | Config option `allowClientClassCreation` defaults to `false` | [#7925](https://github.com/parse-community/parse-server/pull/7925) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - |
|
||||
| DEPPS6 | Auth providers disabled by default | [#7953](https://github.com/parse-community/parse-server/pull/7953) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - |
|
||||
| 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 | - |
|
||||
| DEPPS1 | Native MongoDB syntax in aggregation pipeline | [#7338](https://github.com/parse-community/parse-server/issues/7338) | 5.0.0 (2022) | 6.0.0 (2023) | deprecated | - |
|
||||
| DEPPS2 | Config option `directAccess` defaults to `true` | [#6636](https://github.com/parse-community/parse-server/pull/6636) | 5.0.0 (2022) | 6.0.0 (2023) | deprecated | - |
|
||||
| DEPPS3 | Config option `enforcePrivateUsers` defaults to `true` | [#7319](https://github.com/parse-community/parse-server/pull/7319) | 5.0.0 (2022) | 6.0.0 (2023) | deprecated | - |
|
||||
| DEPPS4 | Remove convenience method for http request `Parse.Cloud.httpRequest` | [#7589](https://github.com/parse-community/parse-server/pull/7589) | 5.0.0 (2022) | 6.0.0 (2023) | deprecated | - |
|
||||
| DEPPS5 | Config option `allowClientClassCreation` defaults to `false` | [#7925](https://github.com/parse-community/parse-server/pull/7925) | 5.3.0 (2022) | 7.0.0 (2024) | deprecated | - |
|
||||
| DEPPS6 | Auth providers disabled by default | [#7953](https://github.com/parse-community/parse-server/pull/7953) | 5.3.0 (2022) | 7.0.0 (2024) | deprecated | - |
|
||||
| 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) | deprecated | - |
|
||||
|
||||
[i_deprecation]: ## "The version and date of the deprecation."
|
||||
[i_removal]: ## "The version and date of the planned removal."
|
||||
|
||||
+21
-19
@@ -1,13 +1,10 @@
|
||||
############################################################
|
||||
# Build stage
|
||||
############################################################
|
||||
FROM node:20.14.0-alpine3.20 AS build
|
||||
|
||||
RUN apk --no-cache add \
|
||||
build-base \
|
||||
git \
|
||||
python3
|
||||
FROM node:lts-alpine as build
|
||||
|
||||
RUN apk update; \
|
||||
apk add git;
|
||||
WORKDIR /tmp
|
||||
|
||||
# Copy package.json first to benefit from layer caching
|
||||
@@ -16,32 +13,37 @@ COPY package*.json ./
|
||||
# Copy src to have config files for install
|
||||
COPY . .
|
||||
|
||||
# Install without scripts
|
||||
RUN npm ci --omit=dev --ignore-scripts \
|
||||
# Copy production node_modules aside for later
|
||||
&& cp -R node_modules prod_node_modules \
|
||||
# Install all dependencies
|
||||
&& npm ci \
|
||||
# Run build steps
|
||||
&& npm run build
|
||||
# Clean npm cache; added to fix an issue with the install process
|
||||
RUN npm cache clean --force
|
||||
|
||||
# Install all dependencies
|
||||
RUN npm ci
|
||||
|
||||
# Run build steps
|
||||
RUN npm run build
|
||||
|
||||
############################################################
|
||||
# Release stage
|
||||
############################################################
|
||||
FROM node:20.14.0-alpine3.20 AS release
|
||||
FROM node:lts-alpine as release
|
||||
|
||||
RUN apk update; \
|
||||
apk add git;
|
||||
|
||||
VOLUME /parse-server/cloud /parse-server/config
|
||||
|
||||
WORKDIR /parse-server
|
||||
|
||||
# Copy build stage folders
|
||||
COPY --from=build /tmp/prod_node_modules /parse-server/node_modules
|
||||
COPY --from=build /tmp/lib lib
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
# Clean npm cache; added to fix an issue with the install process
|
||||
RUN npm cache clean --force
|
||||
RUN npm ci --production --ignore-scripts
|
||||
|
||||
COPY bin bin
|
||||
COPY public_html public_html
|
||||
COPY views views
|
||||
COPY --from=build /tmp/lib lib
|
||||
RUN mkdir -p logs && chown -R node: logs
|
||||
|
||||
ENV PORT=1337
|
||||
|
||||
@@ -1,176 +1,34 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
BSD License
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
For Parse Server software
|
||||
|
||||
1. Definitions.
|
||||
Copyright (c) 2015-present, Parse, LLC. All rights reserved.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
* Neither the name Parse nor the names of its contributors may be used to
|
||||
endorse or promote products derived from this software without specific
|
||||
prior written permission.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
-----
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
As of April 5, 2017, Parse, LLC has transferred this code to the parse-community organization, and will no longer be contributing to or distributing this code.
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
Parse Server
|
||||
|
||||
Copyright 2015-present Parse Platform
|
||||
|
||||
This product includes software developed at Parse Platform.
|
||||
www.parseplatform.org
|
||||
|
||||
---
|
||||
|
||||
As of April 5, 2017, Parse, LLC has transferred this code to the Parse Platform organization, and will no longer be contributing to or distributing this code.
|
||||
@@ -0,0 +1,37 @@
|
||||
Additional Grant of Patent Rights Version 2
|
||||
|
||||
"Software" means the Parse Server software distributed by Parse, LLC.
|
||||
|
||||
Parse, LLC. ("Parse") hereby grants to each recipient of the Software
|
||||
("you") a perpetual, worldwide, royalty-free, non-exclusive, irrevocable
|
||||
(subject to the termination provision below) license under any Necessary
|
||||
Claims, to make, have made, use, sell, offer to sell, import, and otherwise
|
||||
transfer the Software. For avoidance of doubt, no license is granted under
|
||||
Parse’s rights in any patent claims that are infringed by (i) modifications
|
||||
to the Software made by you or any third party or (ii) the Software in
|
||||
combination with any software or other technology.
|
||||
|
||||
The license granted hereunder will terminate, automatically and without notice,
|
||||
if you (or any of your subsidiaries, corporate affiliates or agents) initiate
|
||||
directly or indirectly, or take a direct financial interest in, any Patent
|
||||
Assertion: (i) against Parse or any of its subsidiaries or corporate
|
||||
affiliates, (ii) against any party if such Patent Assertion arises in whole or
|
||||
in part from any software, technology, product or service of Parse or any of
|
||||
its subsidiaries or corporate affiliates, or (iii) against any party relating
|
||||
to the Software. Notwithstanding the foregoing, if Parse or any of its
|
||||
subsidiaries or corporate affiliates files a lawsuit alleging patent
|
||||
infringement against you in the first instance, and you respond by filing a
|
||||
patent infringement counterclaim in that lawsuit against that party that is
|
||||
unrelated to the Software, the license granted hereunder will not terminate
|
||||
under section (i) of this paragraph due to such counterclaim.
|
||||
|
||||
A "Necessary Claim" is a claim of a patent owned by Parse that is
|
||||
necessarily infringed by the Software standing alone.
|
||||
|
||||
A "Patent Assertion" is any lawsuit or other action alleging direct, indirect,
|
||||
or contributory infringement or inducement to infringe any patent, including a
|
||||
cross-claim or counterclaim.
|
||||
|
||||
-----
|
||||
|
||||
As of April 5, 2017, Parse, LLC has transferred this code to the parse-community organization, and will no longer be contributing to or distributing this code.
|
||||
@@ -1,17 +1,15 @@
|
||||

|
||||
|
||||
---
|
||||
|
||||
|
||||
[](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://snyk.io/test/github/parse-community/parse-server)
|
||||
[](https://app.codecov.io/github/parse-community/parse-server/tree/alpha)
|
||||
[](https://codecov.io/github/parse-community/parse-server?branch=alpha)
|
||||
[](https://github.com/parse-community/parse-dashboard/releases)
|
||||
|
||||
[](https://nodejs.org)
|
||||
[](https://www.mongodb.com)
|
||||
[](https://www.postgresql.org)
|
||||
[](https://nodejs.org)
|
||||
[](https://www.mongodb.com)
|
||||
[](https://www.postgresql.org)
|
||||
|
||||
[](https://www.npmjs.com/package/parse-server)
|
||||
[](https://www.npmjs.com/package/parse-server)
|
||||
@@ -19,6 +17,7 @@
|
||||
|
||||
[][open-collective-link]
|
||||
[][open-collective-link]
|
||||
[![License][license-svg]][license-link]
|
||||
[](https://community.parseplatform.org/c/parse-server)
|
||||
[](https://twitter.com/intent/follow?screen_name=ParsePlatform)
|
||||
[](https://chat.parseplatform.org)
|
||||
@@ -39,7 +38,7 @@ A big *thank you* 🙏 to our [sponsors](#sponsors) and [backers](#backers) who
|
||||
|
||||
---
|
||||
|
||||
- [Flavors \& Branches](#flavors--branches)
|
||||
- [Flavors & Branches](#flavors--branches)
|
||||
- [Long Term Support](#long-term-support)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Running Parse Server](#running-parse-server)
|
||||
@@ -54,12 +53,9 @@ A big *thank you* 🙏 to our [sponsors](#sponsors) and [backers](#backers) who
|
||||
- [Running Parse Server elsewhere](#running-parse-server-elsewhere)
|
||||
- [Sample Application](#sample-application)
|
||||
- [Parse Server + Express](#parse-server--express)
|
||||
- [Parse Server Health](#parse-server-health)
|
||||
- [Status Values](#status-values)
|
||||
- [Configuration](#configuration)
|
||||
- [Basic Options](#basic-options)
|
||||
- [Client Key Options](#client-key-options)
|
||||
- [Access Scopes](#access-scopes)
|
||||
- [Email Verification and Password Reset](#email-verification-and-password-reset)
|
||||
- [Password and Account Policy](#password-and-account-policy)
|
||||
- [Custom Routes](#custom-routes)
|
||||
@@ -91,6 +87,7 @@ A big *thank you* 🙏 to our [sponsors](#sponsors) and [backers](#backers) who
|
||||
- [Using automatically generated operations](#using-automatically-generated-operations)
|
||||
- [Customizing your GraphQL Schema](#customizing-your-graphql-schema)
|
||||
- [Learning more](#learning-more)
|
||||
- [Upgrading to Parse Server 3.0](#upgrading-to-parse-server-30)
|
||||
- [Contributing](#contributing)
|
||||
- [Contributors](#contributors)
|
||||
- [Sponsors](#sponsors)
|
||||
@@ -101,13 +98,13 @@ 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 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.
|
||||
- The long-term-support (LTS) branches are named `release-<version>.x.x`, for example `release-4.x.x`. LTS branches do not have pre-release branches.
|
||||
|
||||
## Long Term Support
|
||||
|
||||
Long-Term-Support (LTS) is provided for the previous Parse Server major version. For example, Parse Server 5.x will receive security updates until Parse Server 6.x is superseded by Parse Server 7.x and becomes the new LTS version. While the current major version is published on branch `release`, a LTS version is published on branch `release-#.x.x`, for example `release-5.x.x` for the Parse Server 5.x LTS branch.
|
||||
Long-Term-Support (LTS) is provided for the previous Parse Server major version. For example, Parse Server 4.x will receive security updates until Parse Server 5.x is superseded by Parse Server 6.x and becomes the new LTS version. While the current major version is published on branch `release`, a LTS version is published on branch `release-#.x.x`, for example `release-4.x.x` for the Parse Server 4.x LTS branch.
|
||||
|
||||
⚠️ LTS versions are provided to help you transition as soon as possible to the current major version. While we aim to fix security vulnerabilities in the LTS version, our main focus is on developing the current major version and preparing the next major release. Therefore we may leave certain vulnerabilities up to the community to fix. Search for [pull requests with the specific LTS base branch](https://github.com/parse-community/parse-server/pulls?q=is%3Aopen+is%3Apr+base%3Arelease-5.x.x) to see the current open vulnerabilities for that LTS branch.
|
||||
⚠️ LTS versions are provided to help you transition as soon as possible to the current major version. While we aim to fix security vulnerabilities in the LTS version, our main focus is on developing the current major version and preparing the next major release. Therefore we may leave certain vulnerabilities up to the community to fix. Search for [pull requests with the specific LTS base branch](https://github.com/parse-community/parse-server/pulls?q=is%3Aopen+is%3Apr+base%3Arelease-4.x.x) to see the current open vulnerabilities for that LTS branch.
|
||||
|
||||
# Getting Started
|
||||
|
||||
@@ -129,21 +126,23 @@ Parse Server is continuously tested with the most recent releases of Node.js to
|
||||
|
||||
| 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 |
|
||||
| Node.js 12 | 12.22.11 | April 2022 | ✅ Yes |
|
||||
| Node.js 14 | 14.19.1 | April 2023 | ✅ Yes |
|
||||
| Node.js 16 | 16.14.2 | April 2024 | ✅ Yes |
|
||||
| Node.js 17 | 17.9.0 | June 2022 | ✅ Yes |
|
||||
| Node.js 18 | 18.1.0 | April 2025 | ✅ Yes |
|
||||
|
||||
#### 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.
|
||||
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. We consider the end-of-life date of a MongoDB "rapid release" to be the same as its major version release.
|
||||
|
||||
| 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 | Latest Version | End-of-Life | Compatible |
|
||||
|-------------|----------------|---------------|--------------|
|
||||
| MongoDB 4.0 | 4.0.28 | April 2022 | ✅ Yes |
|
||||
| MongoDB 4.2 | 4.2.19 | April 2023 | ✅ Yes |
|
||||
| MongoDB 4.4 | 4.4.13 | February 2024 | ✅ Yes |
|
||||
| MongoDB 5 | 5.3.2 | October 2024 | ✅ Yes |
|
||||
| MongoDB 6 | 6.0.2 | July 2025 | ✅ Yes |
|
||||
|
||||
#### PostgreSQL
|
||||
|
||||
@@ -151,10 +150,11 @@ Parse Server is continuously tested with the most recent releases of PostgreSQL
|
||||
|
||||
| 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 |
|
||||
| Postgres 11 | 3.0, 3.1, 3.2, 3.3 | November 2023 | <= 5.x (2022) | ✅ Yes |
|
||||
| Postgres 12 | 3.3 | November 2024 | <= 5.x (2022) | ✅ Yes |
|
||||
| Postgres 13 | 3.3 | November 2025 | <= 6.x (2023) | ✅ Yes |
|
||||
| Postgres 14 | 3.3 | November 2026 | <= 7.x (2024) | ✅ Yes |
|
||||
| Postgres 15 | 3.3 | November 2027 | <= 8.x (2025) | ✅ Yes |
|
||||
|
||||
### Locally
|
||||
|
||||
@@ -282,11 +282,11 @@ We have provided a basic [Node.js application](https://github.com/parse-communit
|
||||
You can also create an instance of Parse Server, and mount it on a new or existing Express website:
|
||||
|
||||
```js
|
||||
const express = require('express');
|
||||
const ParseServer = require('parse-server').ParseServer;
|
||||
const app = express();
|
||||
var express = require('express');
|
||||
var ParseServer = require('parse-server').ParseServer;
|
||||
var app = express();
|
||||
|
||||
const server = new ParseServer({
|
||||
var api = new ParseServer({
|
||||
databaseURI: 'mongodb://localhost:27017/dev', // Connection string for your MongoDB database
|
||||
cloud: './cloud/main.js', // Path to your Cloud Code
|
||||
appId: 'myAppId',
|
||||
@@ -295,45 +295,21 @@ const server = new ParseServer({
|
||||
serverURL: 'http://localhost:1337/parse' // Don't forget to change to https if needed
|
||||
});
|
||||
|
||||
// Start server
|
||||
await server.start();
|
||||
|
||||
// Serve the Parse API on the /parse URL prefix
|
||||
app.use('/parse', server.app);
|
||||
app.use('/parse', api);
|
||||
|
||||
app.listen(1337, function() {
|
||||
console.log('parse-server-example running on port 1337.');
|
||||
});
|
||||
```
|
||||
|
||||
For a full list of available options, run `parse-server --help` or take a look at [Parse Server Configurations][server-options].
|
||||
|
||||
## Parse Server Health
|
||||
|
||||
Check the Parse Server health by sending a request to the `/parse/health` endpoint.
|
||||
|
||||
The response looks like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok"
|
||||
}
|
||||
```
|
||||
|
||||
### Status Values
|
||||
|
||||
| Value | Description |
|
||||
|---------------|-----------------------------------------------------------------------------|
|
||||
| `initialized` | The server has been created but the `start` method has not been called yet. |
|
||||
| `starting` | The server is starting up. |
|
||||
| `ok` | The server started and is running. |
|
||||
| `error` | There was a startup error, see the logs for details. |
|
||||
For a full list of available options, run `parse-server --help` or take a look at [Parse Server Configurations](http://parseplatform.org/parse-server/api/master/ParseServerOptions.html).
|
||||
|
||||
# Configuration
|
||||
|
||||
Parse Server can be configured using the following options. You may pass these as parameters when running a standalone `parse-server`, or by loading a configuration file in JSON format using `parse-server path/to/configuration.json`. If you're using Parse Server on Express, you may also pass these to the `ParseServer` object as options.
|
||||
|
||||
For the full list of available options, run `parse-server --help` or take a look at [Parse Server Configurations][server-options].
|
||||
For the full list of available options, run `parse-server --help` or take a look at [Parse Server Configurations](http://parseplatform.org/parse-server/api/master/ParseServerOptions.html).
|
||||
|
||||
## Basic Options
|
||||
|
||||
@@ -354,20 +330,9 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo
|
||||
* `restAPIKey`
|
||||
* `dotNetKey`
|
||||
|
||||
## Access Scopes
|
||||
|
||||
| Scope | Internal data | Read-only data <sub>(1)</sub> | Custom data | Restricted by CLP, ACL | Key |
|
||||
|----------------|---------------|-------------------------------|-------------|------------------------|---------------------|
|
||||
| Internal | r/w | r/w | r/w | no | `maintenanceKey` |
|
||||
| Master | -/- | r/- | r/w | no | `masterKey` |
|
||||
| ReadOnlyMaster | -/- | r/- | r/- | no | `readOnlyMasterKey` |
|
||||
| Session | -/- | r/- | r/w | yes | `sessionToken` |
|
||||
|
||||
<sub>(1) `Parse.Object.createdAt`, `Parse.Object.updatedAt`.</sub>
|
||||
|
||||
## Email Verification and Password Reset
|
||||
|
||||
Verifying user email addresses and enabling password reset via email requires an email adapter. There are many email adapters provided and maintained by the community. The following is an example configuration with an example email adapter. See the [Parse Server Options][server-options] for more details and a full list of available options.
|
||||
Verifying user email addresses and enabling password reset via email requires an email adapter. There are many email adapters provided and maintained by the community. The following is an example configuration with an example email adapter. See the [Parse Server Options](https://parseplatform.org/parse-server/api/master/ParseServerOptions.html) for more details and a full list of available options.
|
||||
|
||||
```js
|
||||
const server = ParseServer({
|
||||
@@ -407,7 +372,7 @@ Email adapters contributed by the community:
|
||||
|
||||
## Password and Account Policy
|
||||
|
||||
Set a password and account policy that meets your security requirements. The following is an example configuration. See the [Parse Server Options][server-options] for more details and a full list of available options.
|
||||
Set a password and account policy that meets your security requirements. The following is an example configuration. See the [Parse Server Options](https://parseplatform.org/parse-server/api/master/ParseServerOptions.html) for more details and a full list of available options.
|
||||
|
||||
```js
|
||||
const server = ParseServer({
|
||||
@@ -424,7 +389,7 @@ const server = ParseServer({
|
||||
},
|
||||
|
||||
// The password policy
|
||||
passwordPolicy: {
|
||||
passwordPolicy: {
|
||||
// Enforce a password of at least 8 characters which contain at least 1 lower case, 1 upper case and 1 digit
|
||||
validatorPattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{8,})/,
|
||||
// Do not allow the username as part of the password
|
||||
@@ -465,7 +430,7 @@ const api = new ParseServer({
|
||||
|
||||
The above route can be invoked by sending a `GET` request to:
|
||||
`https://[parseServerPublicUrl]/[parseMount]/[pagesEndpoint]/[appId]/[customRoute]`
|
||||
|
||||
|
||||
The `handler` receives the `request` and returns a `custom_page.html` webpage from the `pages.pagesPath` directory as response. The advantage of building a custom route this way is that it automatically makes use of Parse Server's built-in capabilities, such as [page localization](#pages) and [dynamic placeholders](#dynamic-placeholders).
|
||||
|
||||
### Reserved Paths
|
||||
@@ -496,7 +461,7 @@ The following paths are already used by Parse Server's built-in features and are
|
||||
It’s possible to change the default pages of the app and redirect the user to another path or domain.
|
||||
|
||||
```js
|
||||
const server = ParseServer({
|
||||
var server = ParseServer({
|
||||
...otherOptions,
|
||||
|
||||
customPages: {
|
||||
@@ -553,7 +518,7 @@ Parse Server allows developers to choose from several options when hosting files
|
||||
`GridFSBucketAdapter` is used by default and requires no setup, but if you're interested in using Amazon S3, Google Cloud Storage, or local file storage, additional configuration information is available in the [Parse Server guide](http://docs.parseplatform.org/parse-server/guide/#configuring-file-adapters).
|
||||
|
||||
## Idempotency Enforcement
|
||||
|
||||
|
||||
**Caution, this is an experimental feature that may not be appropriate for production.**
|
||||
|
||||
This feature deduplicates identical requests that are received by Parse Server multiple times, typically due to network issues or network adapter access restrictions on mobile operating systems.
|
||||
@@ -886,7 +851,7 @@ Then, create an `index.js` file with the following content:
|
||||
|
||||
```js
|
||||
const express = require('express');
|
||||
const { ParseServer, ParseGraphQLServer } = require('parse-server');
|
||||
const { default: ParseServer, ParseGraphQLServer } = require('parse-server');
|
||||
|
||||
const app = express();
|
||||
|
||||
@@ -910,7 +875,6 @@ app.use('/parse', parseServer.app); // (Optional) Mounts the REST API
|
||||
parseGraphQLServer.applyGraphQL(app); // Mounts the GraphQL API
|
||||
parseGraphQLServer.applyPlayground(app); // (Optional) Mounts the GraphQL Playground - do NOT use in Production
|
||||
|
||||
await parseServer.start();
|
||||
app.listen(1337, function() {
|
||||
console.log('REST API running on http://localhost:1337/parse');
|
||||
console.log('GraphQL API running on http://localhost:1337/graphql');
|
||||
@@ -1152,6 +1116,12 @@ You also have a very powerful tool inside your GraphQL Playground. Please look a
|
||||
|
||||
Additionally, the [GraphQL Learn Section](https://graphql.org/learn/) is a very good source to learn more about the power of the GraphQL language.
|
||||
|
||||
# Upgrading to Parse Server 3.0
|
||||
|
||||
Starting Parse Server 3.0, Parse Server uses the Parse JavaScript SDK 2.0. In short, the Parse JavaScript SDK 2.0 removes the backbone style callbacks as well as the `Parse.Promise` object in favor of native promises. All the Cloud Code interfaces also have been updated to reflect those changes, and all backbone style response objects are removed and replaced by promise style resolution.
|
||||
|
||||
We have written up a [migration guide](3.0.0.md) to help you transition to the next major release.
|
||||
|
||||
# Contributing
|
||||
|
||||
Please see the [Contributing Guide](CONTRIBUTING.md).
|
||||
@@ -1203,8 +1173,13 @@ Support us with a monthly donation and help us continue our activities. [Become
|
||||
|
||||
<a href="https://opencollective.com/parse-server#backers" target="_blank"><img src="https://opencollective.com/parse-server/backers.svg?width=890" /></a>
|
||||
|
||||
-----
|
||||
|
||||
As of April 5, 2017, Parse, LLC has transferred this code to the parse-community organization, and will no longer be contributing to or distributing this code.
|
||||
|
||||
[license-svg]: https://img.shields.io/badge/license-BSD-lightgrey.svg
|
||||
[license-link]: LICENSE
|
||||
[open-collective-link]: https://opencollective.com/parse-server
|
||||
[log_release]: https://github.com/parse-community/parse-server/blob/release/changelogs/CHANGELOG_release.md
|
||||
[log_beta]: https://github.com/parse-community/parse-server/blob/beta/changelogs/CHANGELOG_beta.md
|
||||
[log_alpha]: https://github.com/parse-community/parse-server/blob/alpha/changelogs/CHANGELOG_alpha.md
|
||||
[server-options] http://parseplatform.org/parse-server/api/release/ParseServerOptions.html
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,249 +1,3 @@
|
||||
# [7.2.0-beta.1](https://github.com/parse-community/parse-server/compare/7.1.0...7.2.0-beta.1) (2024-07-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Invalid push notification tokens are not cleaned up from database for FCM API v2 ([#9173](https://github.com/parse-community/parse-server/issues/9173)) ([284da09](https://github.com/parse-community/parse-server/commit/284da09f4546356b37511a589fb5f64a3efffe79))
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for dot notation on array fields of Parse Object ([#9115](https://github.com/parse-community/parse-server/issues/9115)) ([cf4c880](https://github.com/parse-community/parse-server/commit/cf4c8807b9da87a0a5f9c94e5bdfcf17cda80cf4))
|
||||
* Upgrade to @parse/push-adapter 6.4.0 ([#9182](https://github.com/parse-community/parse-server/issues/9182)) ([ef1634b](https://github.com/parse-community/parse-server/commit/ef1634bf1f360429108d29b08032fc7961ff96a1))
|
||||
* Upgrade to Parse JS SDK 5.3.0 ([#9180](https://github.com/parse-community/parse-server/issues/9180)) ([dca187f](https://github.com/parse-community/parse-server/commit/dca187f91b93cbb362b22a3fb9ee38451799ff13))
|
||||
|
||||
# [7.1.0-beta.1](https://github.com/parse-community/parse-server/compare/7.0.0...7.1.0-beta.1) (2024-06-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `Parse.Cloud.startJob` and `Parse.Push.send` not returning status ID when setting Parse Server option `directAccess: true` ([#8766](https://github.com/parse-community/parse-server/issues/8766)) ([5b0efb2](https://github.com/parse-community/parse-server/commit/5b0efb22efe94c47f243cf8b1e6407ed5c5a67d3))
|
||||
* `Required` option not handled correctly for special fields (File, GeoPoint, Polygon) on GraphQL API mutations ([#8915](https://github.com/parse-community/parse-server/issues/8915)) ([907ad42](https://github.com/parse-community/parse-server/commit/907ad4267c228d26cfcefe7848b30ce85ba7ff8f))
|
||||
* Facebook Limited Login not working due to incorrect domain in JWT validation ([#9122](https://github.com/parse-community/parse-server/issues/9122)) ([9d0bd2b](https://github.com/parse-community/parse-server/commit/9d0bd2badd6e5f7429d1af00b118225752e5d86a))
|
||||
* Live query throws error when constraint `notEqualTo` is set to `null` ([#8835](https://github.com/parse-community/parse-server/issues/8835)) ([11d3e48](https://github.com/parse-community/parse-server/commit/11d3e484df862224c15d20f6171514948981ea90))
|
||||
* Parse Server option `extendSessionOnUse` not working for session lengths < 24 hours ([#9113](https://github.com/parse-community/parse-server/issues/9113)) ([0a054e6](https://github.com/parse-community/parse-server/commit/0a054e6b541fd5ab470bf025665f5f7d2acedaa0))
|
||||
* Rate limiting can fail when using Parse Server option `rateLimit.redisUrl` with clusters ([#8632](https://github.com/parse-community/parse-server/issues/8632)) ([c277739](https://github.com/parse-community/parse-server/commit/c27773962399f8e27691e3b8087e7e1d59516efd))
|
||||
* SQL injection when using Parse Server with PostgreSQL; fixes security vulnerability [GHSA-c2hr-cqg6-8j6r](https://github.com/parse-community/parse-server/security/advisories/GHSA-c2hr-cqg6-8j6r) ([#9167](https://github.com/parse-community/parse-server/issues/9167)) ([2edf1e4](https://github.com/parse-community/parse-server/commit/2edf1e4c0363af01e97a7fbc97694f851b7d1ff3))
|
||||
|
||||
### Features
|
||||
|
||||
* Add `silent` log level for Cloud Code ([#8803](https://github.com/parse-community/parse-server/issues/8803)) ([5f81efb](https://github.com/parse-community/parse-server/commit/5f81efb42964c4c2fa8bcafee9446a0122e3ce21))
|
||||
* Add server security check status `security.enableCheck` to Features Router ([#8679](https://github.com/parse-community/parse-server/issues/8679)) ([b07ec15](https://github.com/parse-community/parse-server/commit/b07ec153825882e97cc48dc84072c7f549f3238b))
|
||||
* Prevent Parse Server start in case of unknown option in server configuration ([#8987](https://github.com/parse-community/parse-server/issues/8987)) ([8758e6a](https://github.com/parse-community/parse-server/commit/8758e6abb9dbb68757bddcbd332ad25100c24a0e))
|
||||
* Upgrade to @parse/push-adapter 6.0.0 ([#9066](https://github.com/parse-community/parse-server/issues/9066)) ([18bdbf8](https://github.com/parse-community/parse-server/commit/18bdbf89c53a57648891ef582614ba7c2941e587))
|
||||
* Upgrade to @parse/push-adapter 6.2.0 ([#9127](https://github.com/parse-community/parse-server/issues/9127)) ([ca20496](https://github.com/parse-community/parse-server/commit/ca20496f28e5ec1294a7a23c8559df82b79b2a04))
|
||||
* Upgrade to Parse JS SDK 5.2.0 ([#9128](https://github.com/parse-community/parse-server/issues/9128)) ([665b8d5](https://github.com/parse-community/parse-server/commit/665b8d52d6cf5275179a5e1fb132c934edb53ecc))
|
||||
|
||||
# [7.0.0-beta.1](https://github.com/parse-community/parse-server/compare/6.5.0-beta.1...7.0.0-beta.1) (2024-03-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* CacheAdapter does not connect when using a CacheAdapter with a JSON config ([#8633](https://github.com/parse-community/parse-server/issues/8633)) ([720d24e](https://github.com/parse-community/parse-server/commit/720d24e18540da35d50957f17be878316ec30318))
|
||||
* Conditional email verification not working in some cases if `verifyUserEmails`, `preventLoginWithUnverifiedEmail` set to functions ([#8838](https://github.com/parse-community/parse-server/issues/8838)) ([8e7a6b1](https://github.com/parse-community/parse-server/commit/8e7a6b1480c0117e6c73e7adc5a6619115a04e85))
|
||||
* Deny request if master key is not set in Parse Server option `masterKeyIps` regardless of ACL and CLP ([#8957](https://github.com/parse-community/parse-server/issues/8957)) ([a7b5b38](https://github.com/parse-community/parse-server/commit/a7b5b38418cbed9be3f4a7665f25b97f592663e1))
|
||||
* Docker image not published to Docker Hub on new release ([#8905](https://github.com/parse-community/parse-server/issues/8905)) ([a2ac8d1](https://github.com/parse-community/parse-server/commit/a2ac8d133c71cd7b61e5ef59c4be915cfea85db6))
|
||||
* Docker version releases by removing arm/v6 and arm/v7 support ([#8976](https://github.com/parse-community/parse-server/issues/8976)) ([1f62dd0](https://github.com/parse-community/parse-server/commit/1f62dd0f4e107b22a387692558a042ee26ce8703))
|
||||
* GraphQL file upload fails in case of use of pointer or relation ([#8721](https://github.com/parse-community/parse-server/issues/8721)) ([1aba638](https://github.com/parse-community/parse-server/commit/1aba6382c873fb489d4a898d301e6da9fb6aa61b))
|
||||
* Improve PostgreSQL injection detection; fixes security vulnerability [GHSA-6927-3vr9-fxf2](https://github.com/parse-community/parse-server/security/advisories/GHSA-6927-3vr9-fxf2) which affects Parse Server deployments using a Postgres database ([#8961](https://github.com/parse-community/parse-server/issues/8961)) ([cbefe77](https://github.com/parse-community/parse-server/commit/cbefe770a7260b54748a058b8a7389937dc35833))
|
||||
* Incomplete user object in `verifyEmail` function if both username and email are changed ([#8889](https://github.com/parse-community/parse-server/issues/8889)) ([1eb95ae](https://github.com/parse-community/parse-server/commit/1eb95aeb41a96250e582d79a703f6adcb403c08b))
|
||||
* Parse Server option `emailVerifyTokenReuseIfValid: true` generates new token on every email verification request ([#8885](https://github.com/parse-community/parse-server/issues/8885)) ([0023ce4](https://github.com/parse-community/parse-server/commit/0023ce448a5e9423337d0e1a25648bde1156bc95))
|
||||
* Parse Server option `fileExtensions` default value rejects file extensions that are less than 3 or more than 4 characters long ([#8699](https://github.com/parse-community/parse-server/issues/8699)) ([2760381](https://github.com/parse-community/parse-server/commit/276038118377c2b22381bcd8d30337203822121b))
|
||||
* Server crashes on invalid Cloud Function or Cloud Job name; fixes security vulnerability [GHSA-6hh7-46r2-vf29](https://github.com/parse-community/parse-server/security/advisories/GHSA-6hh7-46r2-vf29) ([#9024](https://github.com/parse-community/parse-server/issues/9024)) ([9f6e342](https://github.com/parse-community/parse-server/commit/9f6e3429d3b326cf4e2994733c618d08032fac6e))
|
||||
* Server crashes when receiving an array of `Parse.Pointer` in the request body ([#8784](https://github.com/parse-community/parse-server/issues/8784)) ([66e3603](https://github.com/parse-community/parse-server/commit/66e36039d8af654cfa0284666c0ddd94975dcb52))
|
||||
* Username is `undefined` in email verification link on email change ([#8887](https://github.com/parse-community/parse-server/issues/8887)) ([e315c13](https://github.com/parse-community/parse-server/commit/e315c137bf41bedfa8f0df537f2c3f6ab45b7e60))
|
||||
|
||||
### Features
|
||||
|
||||
* Add `installationId` to arguments for `verifyUserEmails`, `preventLoginWithUnverifiedEmail` ([#8836](https://github.com/parse-community/parse-server/issues/8836)) ([a22dbe1](https://github.com/parse-community/parse-server/commit/a22dbe16d5ac0090608f6caaf0ebd134925b7fd4))
|
||||
* Add `installationId`, `ip`, `resendRequest` to arguments passed to `verifyUserEmails` on verification email request ([#8873](https://github.com/parse-community/parse-server/issues/8873)) ([8adcbee](https://github.com/parse-community/parse-server/commit/8adcbee11283d3e95179ca2047e2615f52c18806))
|
||||
* Add `Parse.User` as function parameter to Parse Server options `verifyUserEmails`, `preventLoginWithUnverifiedEmail` on login ([#8850](https://github.com/parse-community/parse-server/issues/8850)) ([972f630](https://github.com/parse-community/parse-server/commit/972f6300163b3cd7d95eeb95986e8322c95f821c))
|
||||
* Add password validation via POST request for user with unverified email using master key and option `ignoreEmailVerification` ([#8895](https://github.com/parse-community/parse-server/issues/8895)) ([633a9d2](https://github.com/parse-community/parse-server/commit/633a9d25e4253e2125bc93c02ee8a37e0f5f7b83))
|
||||
* Add support for MongoDB 7 ([#8761](https://github.com/parse-community/parse-server/issues/8761)) ([3de8494](https://github.com/parse-community/parse-server/commit/3de8494a221991dfd10a74e0a2dc89576265c9b7))
|
||||
* Add support for MongoDB query comment ([#8928](https://github.com/parse-community/parse-server/issues/8928)) ([2170962](https://github.com/parse-community/parse-server/commit/2170962a50fa353ed85eda3f11dce7ee3647b087))
|
||||
* Add support for Node 20, drop support for Node 14, 16 ([#8907](https://github.com/parse-community/parse-server/issues/8907)) ([ced4872](https://github.com/parse-community/parse-server/commit/ced487246ea0ef72a8aa014991f003209b34841e))
|
||||
* Add support for Postgres 16 ([#8898](https://github.com/parse-community/parse-server/issues/8898)) ([99489b2](https://github.com/parse-community/parse-server/commit/99489b22e4f0982e6cb39992974b51aa8d3a31e4))
|
||||
* Allow `Parse.Session.current` on expired session token instead of throwing error ([#8722](https://github.com/parse-community/parse-server/issues/8722)) ([f9dde4a](https://github.com/parse-community/parse-server/commit/f9dde4a9f8a90c63f71172c9bc515b0f6c6d2e4a))
|
||||
* Deprecation DEPPS5: Config option `allowClientClassCreation` defaults to `false` ([#8849](https://github.com/parse-community/parse-server/issues/8849)) ([29624e0](https://github.com/parse-community/parse-server/commit/29624e0fae17161cd412ae58d35a195cfa286cad))
|
||||
* Deprecation DEPPS6: Authentication adapters disabled by default ([#8858](https://github.com/parse-community/parse-server/issues/8858)) ([0cf58eb](https://github.com/parse-community/parse-server/commit/0cf58eb8d60c8e5f485764e154f3214c49eee430))
|
||||
* Deprecation DEPPS7: Remove deprecated Cloud Code file trigger syntax ([#8855](https://github.com/parse-community/parse-server/issues/8855)) ([4e6a375](https://github.com/parse-community/parse-server/commit/4e6a375b5184ae0f7aa256a921eca4021c609435))
|
||||
* Deprecation DEPPS8: Parse Server option `allowExpiredAuthDataToken` defaults to `false` ([#8860](https://github.com/parse-community/parse-server/issues/8860)) ([e29845f](https://github.com/parse-community/parse-server/commit/e29845f8dacac09ce3093d75c0d92330c24389e8))
|
||||
* Deprecation DEPPS9: LiveQuery `fields` option is renamed to `keys` ([#8852](https://github.com/parse-community/parse-server/issues/8852)) ([38983e8](https://github.com/parse-community/parse-server/commit/38983e8e9b5cdbd006f311a2338103624137d013))
|
||||
* Node process exits with error code 1 on uncaught exception to allow custom uncaught exception handling ([#8894](https://github.com/parse-community/parse-server/issues/8894)) ([70c280c](https://github.com/parse-community/parse-server/commit/70c280ca578ff28b5acf92f37fbe06d42a5b34ca))
|
||||
* Switch GraphQL server from Yoga v2 to Apollo v4 ([#8959](https://github.com/parse-community/parse-server/issues/8959)) ([105ae7c](https://github.com/parse-community/parse-server/commit/105ae7c8a57d5a650b243174a80c26bf6db16e28))
|
||||
* Upgrade Parse Server Push Adapter to 5.0.2 ([#8813](https://github.com/parse-community/parse-server/issues/8813)) ([6ef1986](https://github.com/parse-community/parse-server/commit/6ef1986c03a1d84b7e11c05851e5bf9688d88740))
|
||||
* Upgrade to Parse JS SDK 5 ([#9022](https://github.com/parse-community/parse-server/issues/9022)) ([ad4aa83](https://github.com/parse-community/parse-server/commit/ad4aa83983205a0e27639f6ee6a4a5963b67e4b8))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* Improved IP validation performance for `masterKeyIPs`, `maintenanceKeyIPs` ([#8510](https://github.com/parse-community/parse-server/issues/8510)) ([b87daba](https://github.com/parse-community/parse-server/commit/b87daba0671a1b0b7b8d63bc671d665c91a04522))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* The Parse Server option `allowClientClassCreation` defaults to `false`. ([29624e0](29624e0))
|
||||
* A request using the master key will now be rejected as unauthorized if the IP from which the request originates is not set in the Parse Server option `masterKeyIps`, even if the request does not require the master key permission, for example for a public object in a public class class. ([a7b5b38](a7b5b38))
|
||||
* Node process now exits with code 1 on uncaught exceptions, enabling custom handlers that were blocked by Parse Server's default behavior of re-throwing errors. This change may lead to automatic process restarts by the environment, unlike before. ([70c280c](70c280c))
|
||||
* Authentication adapters are disabled by default; to use an authentication adapter it needs to be explicitly enabled in the Parse Server authentication adapter option `auth.<provider>.enabled: true` ([0cf58eb](0cf58eb))
|
||||
* Parse Server option `allowExpiredAuthDataToken` defaults to `false`; a 3rd party authentication token will be validated every time the user tries to log in and the login will fail if the token has expired; the effect of this change may differ for different authentication adapters, depending on the token lifetime and the token refresh logic of the adapter ([e29845f](e29845f))
|
||||
* LiveQuery `fields` option is renamed to `keys` ([38983e8](38983e8))
|
||||
* Cloud Code file trigger syntax has been aligned with object trigger syntax, for example `Parse.Cloud.beforeDeleteFile'` has been changed to `Parse.Cloud.beforeDelete(Parse.File, (request) => {})'` ([4e6a375](4e6a375))
|
||||
* Removes support for Node 14 and 16 ([ced4872](ced4872))
|
||||
* Removes support for Postgres 11 and 12 ([99489b2](99489b2))
|
||||
* The `Parse.User` passed as argument if `verifyUserEmails` is set to a function is renamed from `user` to `object` for consistency with invocations of `verifyUserEmails` on signup or login; the user object is not a plain JavaScript object anymore but an instance of `Parse.User` ([8adcbee](8adcbee))
|
||||
* `Parse.Session.current()` no longer throws an error if the session token is expired, but instead returns the session token with its expiration date to allow checking its validity ([f9dde4a](f9dde4a))
|
||||
* `Parse.Query` no longer supports the BSON type `code`; although this feature was never officially documented, its removal is announced as a breaking change to protect deployments where it might be in use. ([3de8494](3de8494))
|
||||
|
||||
# [6.5.0-beta.1](https://github.com/parse-community/parse-server/compare/6.4.0...6.5.0-beta.1) (2023-11-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Context not passed to Cloud Code Trigger `beforeFind` when using `Parse.Query.include` ([#8765](https://github.com/parse-community/parse-server/issues/8765)) ([7d32d89](https://github.com/parse-community/parse-server/commit/7d32d8934f3ae7af7a7d8b9cc6a829c7d73973d3))
|
||||
* Parse Server option `fileUpload.fileExtensions` fails to determine file extension if filename contains multiple dots ([#8754](https://github.com/parse-community/parse-server/issues/8754)) ([3d6d50e](https://github.com/parse-community/parse-server/commit/3d6d50e0afff18b95fb906914e2cebd3839b517a))
|
||||
* Security bump @babel/traverse from 7.20.5 to 7.23.2 ([#8777](https://github.com/parse-community/parse-server/issues/8777)) ([2d6b3d1](https://github.com/parse-community/parse-server/commit/2d6b3d18499179e99be116f25c0850d3f449509c))
|
||||
* Security upgrade graphql from 16.6.0 to 16.8.1 ([#8758](https://github.com/parse-community/parse-server/issues/8758)) ([71dfd8a](https://github.com/parse-community/parse-server/commit/71dfd8a7ece8c0dd1a66d03bb9420cfd39f4f9b1))
|
||||
|
||||
### Features
|
||||
|
||||
* Add `$setOnInsert` operator to `Parse.Server.database.update` ([#8791](https://github.com/parse-community/parse-server/issues/8791)) ([f630a45](https://github.com/parse-community/parse-server/commit/f630a45aa5e87bc73a81fded061400c199b71a29))
|
||||
* Add compatibility for MongoDB Atlas Serverless and AWS Amazon DocumentDB with collation options `enableCollationCaseComparison`, `transformEmailToLowercase`, `transformUsernameToLowercase` ([#8805](https://github.com/parse-community/parse-server/issues/8805)) ([09fbeeb](https://github.com/parse-community/parse-server/commit/09fbeebba8870e7cf371fb84371a254c7b368620))
|
||||
* Add context to Cloud Code Triggers `beforeLogin` and `afterLogin` ([#8724](https://github.com/parse-community/parse-server/issues/8724)) ([a9c34ef](https://github.com/parse-community/parse-server/commit/a9c34ef1e2c78a42fb8b5fa8d569b7677c74919d))
|
||||
* Allow setting `createdAt` and `updatedAt` during `Parse.Object` creation with maintenance key ([#8696](https://github.com/parse-community/parse-server/issues/8696)) ([77bbfb3](https://github.com/parse-community/parse-server/commit/77bbfb3f186f5651c33ba152f04cff95128eaf2d))
|
||||
|
||||
# [6.4.0-beta.1](https://github.com/parse-community/parse-server/compare/6.3.0...6.4.0-beta.1) (2023-09-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Parse Server option `fileUpload.fileExtensions` does not work with an array of extensions ([#8688](https://github.com/parse-community/parse-server/issues/8688)) ([6a4a00c](https://github.com/parse-community/parse-server/commit/6a4a00ca7af1163ea74b047b85cd6817366b824b))
|
||||
* Redis 4 does not reconnect after unhandled error ([#8706](https://github.com/parse-community/parse-server/issues/8706)) ([2b3d4e5](https://github.com/parse-community/parse-server/commit/2b3d4e5d3c85cd142f85af68dec51a8523548d49))
|
||||
* Remove config logging when launching Parse Server via CLI ([#8710](https://github.com/parse-community/parse-server/issues/8710)) ([ae68f0c](https://github.com/parse-community/parse-server/commit/ae68f0c31b741eeb83379c905c7ddfaa124436ec))
|
||||
* Server does not start via CLI when `auth` option is set ([#8666](https://github.com/parse-community/parse-server/issues/8666)) ([4e2000b](https://github.com/parse-community/parse-server/commit/4e2000bc563324389584ace3c090a5c1a7796a64))
|
||||
|
||||
### Features
|
||||
|
||||
* Add conditional email verification via dynamic Parse Server options `verifyUserEmails`, `sendUserEmailVerification` that now accept functions ([#8425](https://github.com/parse-community/parse-server/issues/8425)) ([44acd6d](https://github.com/parse-community/parse-server/commit/44acd6d9ed157ad4842200c9d01f9c77a05fec3a))
|
||||
* Add property `Parse.Server.version` to determine current version of Parse Server in Cloud Code ([#8670](https://github.com/parse-community/parse-server/issues/8670)) ([a9d376b](https://github.com/parse-community/parse-server/commit/a9d376b61f5b07806eafbda91c4e36c322f09298))
|
||||
* Add TOTP authentication adapter ([#8457](https://github.com/parse-community/parse-server/issues/8457)) ([cc079a4](https://github.com/parse-community/parse-server/commit/cc079a40f6849a0e9bc6fdc811e8649ecb67b589))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* Improve performance of recursive pointer iterations ([#8741](https://github.com/parse-community/parse-server/issues/8741)) ([45a3ed0](https://github.com/parse-community/parse-server/commit/45a3ed0fcf2c0170607505a1550fb15896e705fd))
|
||||
|
||||
# [6.3.0-beta.1](https://github.com/parse-community/parse-server/compare/6.2.0...6.3.0-beta.1) (2023-06-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Cloud Code Trigger `afterSave` executes even if not set ([#8520](https://github.com/parse-community/parse-server/issues/8520)) ([afd0515](https://github.com/parse-community/parse-server/commit/afd0515e207bd947840579d3f245980dffa6f804))
|
||||
* GridFS file storage doesn't work with certain `enableSchemaHooks` settings ([#8467](https://github.com/parse-community/parse-server/issues/8467)) ([d4cda4b](https://github.com/parse-community/parse-server/commit/d4cda4b26c9bde8c812549b8780bea1cfabdb394))
|
||||
* Inaccurate table total row count for PostgreSQL ([#8511](https://github.com/parse-community/parse-server/issues/8511)) ([0823a02](https://github.com/parse-community/parse-server/commit/0823a02fbf80bc88dc403bc47e9f5c6597ea78b4))
|
||||
* LiveQuery server is not shut down properly when `handleShutdown` is called ([#8491](https://github.com/parse-community/parse-server/issues/8491)) ([967700b](https://github.com/parse-community/parse-server/commit/967700bdbc94c74f75ba84d2b3f4b9f3fd2dca0b))
|
||||
* Rate limit feature is incompatible with Node 14 ([#8578](https://github.com/parse-community/parse-server/issues/8578)) ([f911f2c](https://github.com/parse-community/parse-server/commit/f911f2cd3a8c45cd326272dcd681532764a3761e))
|
||||
* Unnecessary log entries by `extendSessionOnUse` ([#8562](https://github.com/parse-community/parse-server/issues/8562)) ([fd6a007](https://github.com/parse-community/parse-server/commit/fd6a0077f2e5cf83d65e52172ae5a950ab0f1eae))
|
||||
|
||||
### Features
|
||||
|
||||
* `extendSessionOnUse` to automatically renew Parse Sessions ([#8505](https://github.com/parse-community/parse-server/issues/8505)) ([6f885d3](https://github.com/parse-community/parse-server/commit/6f885d36b94902fdfea873fc554dee83589e6029))
|
||||
* Add new Parse Server option `preventSignupWithUnverifiedEmail` to prevent returning a user without session token on sign-up with unverified email address ([#8451](https://github.com/parse-community/parse-server/issues/8451)) ([82da308](https://github.com/parse-community/parse-server/commit/82da30842a55980aa90cb7680fbf6db37ee16dab))
|
||||
* Add option to change the log level of logs emitted by Cloud Functions ([#8530](https://github.com/parse-community/parse-server/issues/8530)) ([2caea31](https://github.com/parse-community/parse-server/commit/2caea310be412d82b04a85716bc769ccc410316d))
|
||||
* Add support for `$eq` query constraint in LiveQuery ([#8614](https://github.com/parse-community/parse-server/issues/8614)) ([656d673](https://github.com/parse-community/parse-server/commit/656d673cf5dea354e4f2b3d4dc2b29a41d311b3e))
|
||||
* Add zones for rate limiting by `ip`, `user`, `session`, `global` ([#8508](https://github.com/parse-community/parse-server/issues/8508)) ([03fba97](https://github.com/parse-community/parse-server/commit/03fba97e0549bfcaeee9f2fa4c9905dbcc91840e))
|
||||
* Allow `Parse.Object` pointers in Cloud Code arguments ([#8490](https://github.com/parse-community/parse-server/issues/8490)) ([28aeda3](https://github.com/parse-community/parse-server/commit/28aeda3f160efcbbcf85a85484a8d26567fa9761))
|
||||
|
||||
### Reverts
|
||||
|
||||
* fix: Inaccurate table total row count for PostgreSQL ([6722110](https://github.com/parse-community/parse-server/commit/6722110f203bc5fdcaa68cdf091cf9e7b48d1cff))
|
||||
|
||||
# [6.1.0-beta.2](https://github.com/parse-community/parse-server/compare/6.1.0-beta.1...6.1.0-beta.2) (2023-05-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* LiveQuery can return incorrectly formatted date ([#8456](https://github.com/parse-community/parse-server/issues/8456)) ([4ce135a](https://github.com/parse-community/parse-server/commit/4ce135a4fe930776044bc8fd786a4e17a0144e03))
|
||||
* Nested date is incorrectly decoded as empty object `{}` when fetching a Parse Object ([#8446](https://github.com/parse-community/parse-server/issues/8446)) ([22d2446](https://github.com/parse-community/parse-server/commit/22d2446dfea2bc339affc20535d181097e152acf))
|
||||
* Parameters missing in `afterFind` trigger of authentication adapters ([#8458](https://github.com/parse-community/parse-server/issues/8458)) ([ce34747](https://github.com/parse-community/parse-server/commit/ce34747e8af54cb0b6b975da38f779a5955d2d59))
|
||||
* Rate limiting across multiple servers via Redis not working ([#8469](https://github.com/parse-community/parse-server/issues/8469)) ([d9e347d](https://github.com/parse-community/parse-server/commit/d9e347d7413f30f58ffbb8397fc8b5ae23be6ff0))
|
||||
|
||||
### Features
|
||||
|
||||
* Add `afterFind` trigger to authentication adapters ([#8444](https://github.com/parse-community/parse-server/issues/8444)) ([c793bb8](https://github.com/parse-community/parse-server/commit/c793bb88e7485743c7ceb65fe419cde75833ff33))
|
||||
* Add rate limiting across multiple servers via Redis ([#8394](https://github.com/parse-community/parse-server/issues/8394)) ([34833e4](https://github.com/parse-community/parse-server/commit/34833e42eec08b812b733be78df0535ab0e096b6))
|
||||
* Allow multiple origins for header `Access-Control-Allow-Origin` ([#8517](https://github.com/parse-community/parse-server/issues/8517)) ([4f15539](https://github.com/parse-community/parse-server/commit/4f15539ac244aa2d393ac5177f7604b43f69e271))
|
||||
* Export `AuthAdapter` to make it available for extension with custom authentication adapters ([#8443](https://github.com/parse-community/parse-server/issues/8443)) ([40c1961](https://github.com/parse-community/parse-server/commit/40c196153b8efa12ae384c1c0092b2ed60a260d6))
|
||||
|
||||
# [6.1.0-beta.1](https://github.com/parse-community/parse-server/compare/6.0.0...6.1.0-beta.1) (2023-03-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Security upgrade jsonwebtoken to 9.0.0 ([#8420](https://github.com/parse-community/parse-server/issues/8420)) ([f5bfe45](https://github.com/parse-community/parse-server/commit/f5bfe4571e82b2b7440d41f3cff0d49937398164))
|
||||
|
||||
### Features
|
||||
|
||||
* Add option `schemaCacheTtl` for schema cache pulling as alternative to `enableSchemaHooks` ([#8436](https://github.com/parse-community/parse-server/issues/8436)) ([b3b76de](https://github.com/parse-community/parse-server/commit/b3b76de71b1d4265689d052e7837c38ec1fa4323))
|
||||
* Add Parse Server option `resetPasswordSuccessOnInvalidEmail` to choose success or error response on password reset with invalid email ([#7551](https://github.com/parse-community/parse-server/issues/7551)) ([e5d610e](https://github.com/parse-community/parse-server/commit/e5d610e5e487ddab86409409ac3d7362aba8f59b))
|
||||
* Deprecate LiveQuery `fields` option in favor of `keys` for semantic consistency ([#8388](https://github.com/parse-community/parse-server/issues/8388)) ([a49e323](https://github.com/parse-community/parse-server/commit/a49e323d5ae640bff1c6603ec37fdaddb9328dd1))
|
||||
|
||||
# [6.0.0-beta.1](https://github.com/parse-community/parse-server/compare/5.4.0...6.0.0-beta.1) (2023-01-31)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `ParseServer.verifyServerUrl` may fail if server response headers are missing; remove unnecessary logging ([#8391](https://github.com/parse-community/parse-server/issues/8391)) ([1c37a7c](https://github.com/parse-community/parse-server/commit/1c37a7cd0715949a70b220a629071c7dab7d5e7b))
|
||||
* Cloud Code trigger `beforeSave` does not work with `Parse.Role` ([#8320](https://github.com/parse-community/parse-server/issues/8320)) ([f29d972](https://github.com/parse-community/parse-server/commit/f29d9720e9b37918fd885c97a31e34c42750e724))
|
||||
* ES6 modules do not await the import of Cloud Code files ([#8368](https://github.com/parse-community/parse-server/issues/8368)) ([a7bd180](https://github.com/parse-community/parse-server/commit/a7bd180cddd784c8735622f22e012c342ad535fb))
|
||||
* Nested objects are encoded incorrectly for MongoDB ([#8209](https://github.com/parse-community/parse-server/issues/8209)) ([1412666](https://github.com/parse-community/parse-server/commit/1412666f75829612de6fb9d7ccae35761c9b75cb))
|
||||
* Parse Server option `masterKeyIps` does not include localhost by default for IPv6 ([#8322](https://github.com/parse-community/parse-server/issues/8322)) ([ab82635](https://github.com/parse-community/parse-server/commit/ab82635b0d4cf323a07ddee51fee587b43dce95c))
|
||||
* Rate limiter may reject requests that contain a session token ([#8399](https://github.com/parse-community/parse-server/issues/8399)) ([c114dc8](https://github.com/parse-community/parse-server/commit/c114dc8831055d74187b9dfb4c9eeb558520237c))
|
||||
* Remove Node 12 and Node 17 support ([#8279](https://github.com/parse-community/parse-server/issues/8279)) ([2546cc8](https://github.com/parse-community/parse-server/commit/2546cc8572bea6610cb9b3c7401d9afac0e3c1d6))
|
||||
* Schema without class level permissions may cause error ([#8409](https://github.com/parse-community/parse-server/issues/8409)) ([aa2cd51](https://github.com/parse-community/parse-server/commit/aa2cd51b703388d925e4572e5c2b2d883c68e49c))
|
||||
* The client IP address may be determined incorrectly in some cases; this fixes a security vulnerability in which the Parse Server option `masterKeyIps` may be circumvented, see [GHSA-vm5r-c87r-pf6x](https://github.com/parse-community/parse-server/security/advisories/GHSA-vm5r-c87r-pf6x) ([#8372](https://github.com/parse-community/parse-server/issues/8372)) ([892040d](https://github.com/parse-community/parse-server/commit/892040dc2f82a3e2abe2824e4b553521b6f894de))
|
||||
* Throwing error in Cloud Code Triggers `afterLogin`, `afterLogout` crashes server ([#8280](https://github.com/parse-community/parse-server/issues/8280)) ([130d290](https://github.com/parse-community/parse-server/commit/130d29074e3f763460e5685d0b9059e5a333caff))
|
||||
|
||||
### Features
|
||||
|
||||
* Access the internal scope of Parse Server using the new `maintenanceKey`; the internal scope contains unofficial and undocumented fields (prefixed with underscore `_`) which are used internally by Parse Server; you may want to manipulate these fields for out-of-band changes such as data migration or correction tasks; changes within the internal scope of Parse Server may happen at any time without notice or changelog entry, it is therefore recommended to look at the source code of Parse Server to understand the effects of manipulating internal fields before using the key; it is discouraged to use the `maintenanceKey` for routine operations in a production environment; see [access scopes](https://github.com/parse-community/parse-server#access-scopes) ([#8212](https://github.com/parse-community/parse-server/issues/8212)) ([f3bcc93](https://github.com/parse-community/parse-server/commit/f3bcc9365cd6f08b0a32c132e8e5ff6d1b650863))
|
||||
* Adapt `verifyServerUrl` for new asynchronous Parse Server start-up states ([#8366](https://github.com/parse-community/parse-server/issues/8366)) ([ffa4974](https://github.com/parse-community/parse-server/commit/ffa4974158615fbff4a2692b9db41dcb50d3f77b))
|
||||
* Add `ParseQuery.watch` to trigger LiveQuery only on update of specific fields ([#8028](https://github.com/parse-community/parse-server/issues/8028)) ([fc92faa](https://github.com/parse-community/parse-server/commit/fc92faac75107b3392eeddd916c4c5b45e3c5e0c))
|
||||
* Add Node 19 support ([#8363](https://github.com/parse-community/parse-server/issues/8363)) ([a4990dc](https://github.com/parse-community/parse-server/commit/a4990dcd29abcb4442f3c424aff482a0a116160f))
|
||||
* Add option to change the log level of the logs emitted by triggers ([#8328](https://github.com/parse-community/parse-server/issues/8328)) ([8f3b694](https://github.com/parse-community/parse-server/commit/8f3b694e39d4a966567e50dbea4d62e954fa5c06))
|
||||
* Add request rate limiter based on IP address ([#8174](https://github.com/parse-community/parse-server/issues/8174)) ([6c79f6a](https://github.com/parse-community/parse-server/commit/6c79f6a69e25e47846e3b0685d6bdfd6b91086b1))
|
||||
* Asynchronous initialization of Parse Server ([#8232](https://github.com/parse-community/parse-server/issues/8232)) ([99fcf45](https://github.com/parse-community/parse-server/commit/99fcf45e55c368de2345b0c4d780e70e0adf0e15))
|
||||
* Improve authentication adapter interface to support multi-factor authentication (MFA), authentication challenges, and provide a more powerful interface for writing custom authentication adapters ([#8156](https://github.com/parse-community/parse-server/issues/8156)) ([5bbf9ca](https://github.com/parse-community/parse-server/commit/5bbf9cade9a527787fd1002072d4013ab5d8db2b))
|
||||
* Reduce Docker image size by improving stages ([#8359](https://github.com/parse-community/parse-server/issues/8359)) ([40810b4](https://github.com/parse-community/parse-server/commit/40810b48ebde8b1f21d2448a3a4de0585b1b5e34))
|
||||
* Remove deprecation `DEPPS1`: Native MongoDB syntax in aggregation pipeline ([#8362](https://github.com/parse-community/parse-server/issues/8362)) ([d0d30c4](https://github.com/parse-community/parse-server/commit/d0d30c4f1394f563724644a8fc81734be538a2c0))
|
||||
* Remove deprecation `DEPPS2`: Config option `directAccess` defaults to true ([#8284](https://github.com/parse-community/parse-server/issues/8284)) ([f535ee6](https://github.com/parse-community/parse-server/commit/f535ee6ec2abba63f702127258ca49fa5b4e08c9))
|
||||
* Remove deprecation `DEPPS3`: Config option `enforcePrivateUsers` defaults to `true` ([#8283](https://github.com/parse-community/parse-server/issues/8283)) ([ed499e3](https://github.com/parse-community/parse-server/commit/ed499e32a21bab9a874a9e5367dc71248ce836c4))
|
||||
* Remove deprecation `DEPPS4`: Remove convenience method for http request `Parse.Cloud.httpRequest` ([#8287](https://github.com/parse-community/parse-server/issues/8287)) ([2d79c08](https://github.com/parse-community/parse-server/commit/2d79c0835b6a9acaf20d5c943d9b4619bb96831c))
|
||||
* Remove support for MongoDB 4.0 ([#8292](https://github.com/parse-community/parse-server/issues/8292)) ([37245f6](https://github.com/parse-community/parse-server/commit/37245f62ce83516b6b95a54b850f0274ef680478))
|
||||
* Restrict use of `masterKey` to localhost by default ([#8281](https://github.com/parse-community/parse-server/issues/8281)) ([6c16021](https://github.com/parse-community/parse-server/commit/6c16021a1f03a70a6d9e68cb64df362d07f3b693))
|
||||
* Upgrade Node Package Manager lock file `package-lock.json` to version 2 ([#8285](https://github.com/parse-community/parse-server/issues/8285)) ([ee72467](https://github.com/parse-community/parse-server/commit/ee7246733d63e4bda20401f7b00262ff03299f20))
|
||||
* Upgrade Redis 3 to 4 ([#8293](https://github.com/parse-community/parse-server/issues/8293)) ([7d622f0](https://github.com/parse-community/parse-server/commit/7d622f06a4347e0ad2cba9a4ec07d8d4fb0f67bc))
|
||||
* Upgrade Redis 3 to 4 for LiveQuery ([#8333](https://github.com/parse-community/parse-server/issues/8333)) ([b2761fb](https://github.com/parse-community/parse-server/commit/b2761fb3786b519d9bbcf35be54309d2d35da1a9))
|
||||
* Upgrade to Parse JavaScript SDK 4 ([#8332](https://github.com/parse-community/parse-server/issues/8332)) ([9092874](https://github.com/parse-community/parse-server/commit/9092874a9a482a24dfdce1dce56615702999d6b8))
|
||||
* Write log entry when request with master key is rejected as outside of `masterKeyIps` ([#8350](https://github.com/parse-community/parse-server/issues/8350)) ([e22b73d](https://github.com/parse-community/parse-server/commit/e22b73d4b700c8ff745aa81726c6680082294b45))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* The Docker image does not contain the git dependency anymore; if you have been using git as a transitive dependency it now needs to be explicitly installed in your Docker file, for example with `RUN apk --no-cache add git` (#8359) ([40810b4](40810b4))
|
||||
* Fields in the internal scope of Parse Server (prefixed with underscore `_`) are only returned using the new `maintenanceKey`; previously the `masterKey` allowed reading of internal fields; see [access scopes](https://github.com/parse-community/parse-server#access-scopes) for a comparison of the keys' access permissions (#8212) ([f3bcc93](f3bcc93))
|
||||
* The method `ParseServer.verifyServerUrl` now returns a promise instead of a callback. ([ffa4974](ffa4974))
|
||||
* The MongoDB aggregation pipeline requires native MongoDB syntax instead of the custom Parse Server syntax; for example pipeline stage names require a leading dollar sign like `$match` and the MongoDB document ID is referenced using `_id` instead of `objectId` (#8362) ([d0d30c4](d0d30c4))
|
||||
* The mechanism to determine the client IP address has been rewritten; to correctly determine the IP address it is now required to set the Parse Server option `trustProxy` accordingly if Parse Server runs behind a proxy server, see the express framework's [trust proxy](https://expressjs.com/en/guide/behind-proxies.html) setting (#8372) ([892040d](892040d))
|
||||
* The Node Package Manager lock file `package-lock.json` is upgraded to version 2; while it is backwards with version 1 for the npm installer, consider this if you run any non-npm analysis tools that use the lock file (#8285) ([ee72467](ee72467))
|
||||
* This release introduces the asynchronous initialization of Parse Server to prevent mounting Parse Server before being ready to receive request; it changes how Parse Server is imported, initialized and started; it also removes the callback `serverStartComplete`; see the [Parse Server 6 migration guide](https://github.com/parse-community/parse-server/blob/alpha/6.0.0.md) for more details (#8232) ([99fcf45](99fcf45))
|
||||
* Nested objects are now properly stored in the database using JSON serialization; previously, due to a bug only top-level objects were serialized, but nested objects were saved as raw JSON; for example, a nested `Date` object was saved as a JSON object like `{ "__type": "Date", "iso": "2020-01-01T00:00:00.000Z" }` instead of its serialized representation `2020-01-01T00:00:00.000Z` (#8209) ([1412666](1412666))
|
||||
* The Parse Server option `enforcePrivateUsers` is set to `true` by default; in previous releases this option defaults to `false`; this change improves the default security configuration of Parse Server (#8283) ([ed499e3](ed499e3))
|
||||
* This release restricts the use of `masterKey` to localhost by default; if you are using Parse Dashboard on a different server to connect to Parse Server you need to add the IP address of the server that hosts Parse Dashboard to this option (#8281) ([6c16021](6c16021))
|
||||
* This release upgrades to Redis 4; if you are using the Redis cache adapter with Parse Server then this is a breaking change as the Redis client options have changed; see the [Redis migration guide](https://github.com/redis/node-redis/blob/redis%404.0.0/docs/v3-to-v4.md) for more details (#8293) ([7d622f0](7d622f0))
|
||||
* This release removes support for MongoDB 4.0; the new minimum supported MongoDB version is 4.2. which also removes support for the deprecated MongoDB MMAPv1 storage engine ([37245f6](37245f6))
|
||||
* Throwing an error in Cloud Code Triggers `afterLogin`, `afterLogout` returns a rejected promise; in previous releases it crashed the server if you did not handle the error on the Node.js process level; consider adapting your code if your app currently handles these errors on the Node.js process level with `process.on('unhandledRejection', ...)` ([130d290](130d290))
|
||||
* Config option `directAccess` defaults to true; set this to `false` in environments where multiple Parse Server instances run behind a load balancer and Parse requests within the current Node.js environment should be routed via the load balancer and distributed as HTTP requests among all instances via the `serverURL`. ([f535ee6](f535ee6))
|
||||
* The convenience method for HTTP requests `Parse.Cloud.httpRequest` is removed; use your preferred 3rd party library for making HTTP requests ([2d79c08](2d79c08))
|
||||
* This release removes Node 12 and Node 17 support ([2546cc8](2546cc8))
|
||||
|
||||
# [5.4.0-beta.1](https://github.com/parse-community/parse-server/compare/5.3.0...5.4.0-beta.1) (2022-10-29)
|
||||
|
||||
|
||||
|
||||
+25
-1993
File diff suppressed because it is too large
Load Diff
+18
-20
@@ -1,7 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
const CiVersionCheck = require('./CiVersionCheck');
|
||||
const { exec } = require('child_process');
|
||||
const mongoVersionList = require('mongodb-version-list');
|
||||
const allNodeVersions = require('all-node-versions');
|
||||
|
||||
async function check() {
|
||||
// Run checks
|
||||
@@ -13,16 +14,14 @@ async function check() {
|
||||
* Check the MongoDB versions used in test environments.
|
||||
*/
|
||||
async function checkMongoDbVersions() {
|
||||
let latestStableVersions = await new Promise((resolve, reject) => {
|
||||
exec('m ls', (error, stdout) => {
|
||||
const releasedVersions = await new Promise((resolve, reject) => {
|
||||
mongoVersionList(function (error, versions) {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve(stdout.trim());
|
||||
resolve(versions);
|
||||
});
|
||||
});
|
||||
latestStableVersions = latestStableVersions.split('\n').map(version => version.trim());
|
||||
|
||||
await new CiVersionCheck({
|
||||
packageName: 'MongoDB',
|
||||
@@ -30,14 +29,13 @@ async function checkMongoDbVersions() {
|
||||
yamlFilePath: './.github/workflows/ci.yml',
|
||||
ciEnvironmentsKeyPath: 'jobs.check-mongo.strategy.matrix.include',
|
||||
ciVersionKey: 'MONGODB_VERSION',
|
||||
releasedVersions: latestStableVersions,
|
||||
latestComponent: CiVersionCheck.versionComponents.patch,
|
||||
releasedVersions,
|
||||
latestComponent: CiVersionCheck.versionComponents.minor,
|
||||
ignoreReleasedVersions: [
|
||||
'<4.2.0', // These versions have reached their end-of-life support date
|
||||
'>=4.3.0 <5.0.0', // Unsupported rapid release versions
|
||||
'>=5.1.0 <6.0.0', // Unsupported rapid release versions
|
||||
'>=6.1.0 <7.0.0', // Unsupported rapid release versions
|
||||
'>=7.1.0 <8.0.0', // Unsupported rapid release versions
|
||||
'<4.0.0', // Versions reached their MongoDB end-of-life support date
|
||||
'~4.1.0', // Development release according to MongoDB support
|
||||
'~4.3.0', // Development release according to MongoDB support
|
||||
'~4.7.0', // Development release according to MongoDB support
|
||||
],
|
||||
}).check();
|
||||
}
|
||||
@@ -46,9 +44,8 @@ async function checkMongoDbVersions() {
|
||||
* Check the Nodejs versions used in test environments.
|
||||
*/
|
||||
async function checkNodeVersions() {
|
||||
const allVersions = (await import('all-node-versions')).default;
|
||||
const { versions } = await allVersions();
|
||||
const nodeVersions = versions.map(version => version.node);
|
||||
const allVersions = await allNodeVersions();
|
||||
const releasedVersions = allVersions.versions;
|
||||
|
||||
await new CiVersionCheck({
|
||||
packageName: 'Node.js',
|
||||
@@ -56,12 +53,13 @@ async function checkNodeVersions() {
|
||||
yamlFilePath: './.github/workflows/ci.yml',
|
||||
ciEnvironmentsKeyPath: 'jobs.check-mongo.strategy.matrix.include',
|
||||
ciVersionKey: 'NODE_VERSION',
|
||||
releasedVersions: nodeVersions,
|
||||
releasedVersions,
|
||||
latestComponent: CiVersionCheck.versionComponents.minor,
|
||||
ignoreReleasedVersions: [
|
||||
'<18.0.0', // These versions have reached their end-of-life support date
|
||||
'>=19.0.0 <20.0.0', // These versions have reached their end-of-life support date
|
||||
'>=21.0.0', // These versions are not officially supported yet
|
||||
'<12.0.0', // These versions have reached their end-of-life support date
|
||||
'>=13.0.0 <14.0.0', // These versions have reached their end-of-life support date
|
||||
'>=15.0.0 <16.0.0', // These versions have reached their end-of-life support date
|
||||
'>=19.0.0', // These versions are not officially supported yet
|
||||
],
|
||||
}).check();
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
const fs = require('fs').promises;
|
||||
const { exec } = require('child_process');
|
||||
const core = require('@actions/core');
|
||||
const util = require('util');
|
||||
(async () => {
|
||||
const [currentDefinitions, currentDocs] = await Promise.all([
|
||||
fs.readFile('./src/Options/Definitions.js', 'utf8'),
|
||||
fs.readFile('./src/Options/docs.js', 'utf8'),
|
||||
]);
|
||||
const execute = util.promisify(exec);
|
||||
await execute('npm run definitions');
|
||||
const [newDefinitions, newDocs] = await Promise.all([
|
||||
fs.readFile('./src/Options/Definitions.js', 'utf8'),
|
||||
fs.readFile('./src/Options/docs.js', 'utf8'),
|
||||
]);
|
||||
if (currentDefinitions !== newDefinitions || currentDocs !== newDocs) {
|
||||
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.'
|
||||
);
|
||||
core.error('Definitions files cannot be updated manually. Please update src/Options/index.js then run `npm run definitions` to generate definitions.');
|
||||
process.exit(1);
|
||||
} else {
|
||||
process.exit(0);
|
||||
}
|
||||
})();
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Read package exclusion list from arguments
|
||||
exclusionList=("$@")
|
||||
|
||||
# Convert exclusion list to grep pattern
|
||||
exclusionPattern=$(printf "|%s" "${exclusionList[@]}")
|
||||
exclusionPattern=${exclusionPattern:1}
|
||||
|
||||
# Get list of all dev dependencies
|
||||
devDeps=$(jq -r '.devDependencies | keys | .[]' package.json)
|
||||
|
||||
# Filter out exclusion list
|
||||
depsToUninstall=$(echo "$devDeps" | grep -Ev "$exclusionPattern")
|
||||
|
||||
# If there are dependencies to uninstall then uninstall them
|
||||
if [ -n "$depsToUninstall" ]; then
|
||||
echo "Uninstalling dev dependencies: $depsToUninstall"
|
||||
npm uninstall $depsToUninstall
|
||||
else
|
||||
echo "No dev dependencies to uninstall"
|
||||
fi
|
||||
+5
-22
@@ -1,16 +1,10 @@
|
||||
{
|
||||
"plugins": ["node_modules/jsdoc-babel", "plugins/markdown"],
|
||||
"babel": {
|
||||
"plugins": ["@babel/plugin-transform-flow-strip-types"]
|
||||
"plugins": ["@babel/plugin-transform-flow-strip-types"]
|
||||
},
|
||||
"source": {
|
||||
"include": [
|
||||
"README.md",
|
||||
"./src/cloud-code",
|
||||
"./src/Options/docs.js",
|
||||
"./src/ParseServer.js",
|
||||
"./src/Adapters"
|
||||
],
|
||||
"include": ["./README.md", "./src/cloud-code", "./src/Options/docs.js", "./src/ParseServer.js", "./src/Adapters"],
|
||||
"excludePattern": "(^|\\/|\\\\)_"
|
||||
},
|
||||
"templates": {
|
||||
@@ -23,18 +17,7 @@
|
||||
"monospaceLinks": false
|
||||
},
|
||||
"opts": {
|
||||
"encoding": "utf8",
|
||||
"readme": "./README.md",
|
||||
"recurse": true,
|
||||
"template": "./node_modules/clean-jsdoc-theme",
|
||||
"theme_opts": {
|
||||
"default_theme": "dark",
|
||||
"title": "<img src='https://raw.githubusercontent.com/parse-community/parse-server/alpha/.github/parse-server-logo.png' class='logo'/>",
|
||||
"create_style": "header, .sidebar-section-title, .sidebar-title { color: #139cee !important } .logo { margin-left : 40px; margin-right: 40px }"
|
||||
}
|
||||
},
|
||||
"markdown": {
|
||||
"hardwrap": false,
|
||||
"idInHeadings": true
|
||||
"template": "node_modules/@parse/minami",
|
||||
"recurse": true
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+10537
-30549
File diff suppressed because it is too large
Load Diff
+69
-75
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "parse-server",
|
||||
"version": "7.3.0-alpha.3",
|
||||
"version": "5.4.2",
|
||||
"description": "An express module providing a Parse-compatible API server",
|
||||
"main": "lib/index.js",
|
||||
"repository": {
|
||||
@@ -13,125 +13,119 @@
|
||||
"public_html/",
|
||||
"views/",
|
||||
"LICENSE",
|
||||
"NOTICE",
|
||||
"PATENTS",
|
||||
"postinstall.js",
|
||||
"README.md"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"license": "BSD-3-Clause",
|
||||
"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-yoga/node": "2.6.0",
|
||||
"@graphql-tools/utils": "8.12.0",
|
||||
"@parse/fs-files-adapter": "3.0.0",
|
||||
"@parse/push-adapter": "6.4.1",
|
||||
"@graphql-tools/merge": "8.3.6",
|
||||
"@graphql-tools/schema": "9.0.4",
|
||||
"@parse/fs-files-adapter": "1.2.2",
|
||||
"@parse/push-adapter": "4.1.2",
|
||||
"bcryptjs": "2.4.3",
|
||||
"body-parser": "1.20.2",
|
||||
"commander": "12.0.0",
|
||||
"body-parser": "1.20.1",
|
||||
"commander": "5.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",
|
||||
"graphql-list-fields": "2.0.4",
|
||||
"graphql-relay": "0.10.1",
|
||||
"express": "4.18.1",
|
||||
"follow-redirects": "1.15.2",
|
||||
"graphql": "16.6.0",
|
||||
"graphql-list-fields": "2.0.2",
|
||||
"graphql-tag": "2.12.6",
|
||||
"graphql-upload": "15.0.2",
|
||||
"graphql-relay": "0.10.0",
|
||||
"intersect": "1.0.1",
|
||||
"jsonwebtoken": "9.0.2",
|
||||
"jwks-rsa": "3.1.0",
|
||||
"ldapjs": "3.0.7",
|
||||
"jsonwebtoken": "9.0.0",
|
||||
"jwks-rsa": "2.1.4",
|
||||
"ldapjs": "2.3.3",
|
||||
"lodash": "4.17.21",
|
||||
"lru-cache": "10.2.2",
|
||||
"lru-cache": "7.12.0",
|
||||
"mime": "3.0.0",
|
||||
"mongodb": "5.9.0",
|
||||
"mongodb": "4.10.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",
|
||||
"parse": "3.4.2",
|
||||
"pg-monitor": "1.5.0",
|
||||
"pg-promise": "10.12.0",
|
||||
"pluralize": "8.0.0",
|
||||
"punycode": "2.3.1",
|
||||
"rate-limit-redis": "4.2.0",
|
||||
"redis": "4.6.13",
|
||||
"semver": "7.6.0",
|
||||
"redis": "3.1.2",
|
||||
"semver": "7.3.8",
|
||||
"subscriptions-transport-ws": "0.11.0",
|
||||
"tv4": "1.3.0",
|
||||
"uuid": "10.0.0",
|
||||
"winston": "3.13.1",
|
||||
"winston-daily-rotate-file": "5.0.0",
|
||||
"ws": "8.18.0"
|
||||
"uuid": "9.0.0",
|
||||
"winston": "3.8.1",
|
||||
"winston-daily-rotate-file": "4.7.1",
|
||||
"ws": "8.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/core": "1.10.1",
|
||||
"@apollo/client": "3.9.11",
|
||||
"@babel/cli": "7.24.8",
|
||||
"@babel/core": "7.24.7",
|
||||
"@babel/plugin-proposal-object-rest-spread": "7.20.7",
|
||||
"@babel/plugin-transform-flow-strip-types": "7.24.7",
|
||||
"@babel/preset-env": "7.24.0",
|
||||
"@saithodev/semantic-release-backmerge": "4.0.1",
|
||||
"@semantic-release/changelog": "6.0.3",
|
||||
"@semantic-release/commit-analyzer": "13.0.0",
|
||||
"@semantic-release/git": "10.0.1",
|
||||
"@semantic-release/github": "10.1.0",
|
||||
"@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",
|
||||
"clean-jsdoc-theme": "4.3.0",
|
||||
"graphql-tag": "2.12.6",
|
||||
"@actions/core": "1.9.1",
|
||||
"@apollo/client": "3.6.1",
|
||||
"@babel/cli": "7.10.0",
|
||||
"@babel/core": "7.10.0",
|
||||
"@babel/plugin-proposal-object-rest-spread": "7.10.0",
|
||||
"@babel/plugin-transform-flow-strip-types": "7.9.0",
|
||||
"@babel/preset-env": "7.10.0",
|
||||
"@parse/minami": "1.0.0",
|
||||
"@saithodev/semantic-release-backmerge": "2.1.2",
|
||||
"@semantic-release/changelog": "5.0.1",
|
||||
"@semantic-release/commit-analyzer": "8.0.1",
|
||||
"@semantic-release/git": "9.0.0",
|
||||
"@semantic-release/github": "7.2.3",
|
||||
"@semantic-release/npm": "7.1.3",
|
||||
"@semantic-release/release-notes-generator": "9.0.3",
|
||||
"all-node-versions": "8.0.0",
|
||||
"apollo-upload-client": "16.0.0",
|
||||
"apollo-utilities": "1.3.4",
|
||||
"babel-eslint": "10.1.0",
|
||||
"bcrypt-nodejs": "0.0.3",
|
||||
"cross-env": "7.0.2",
|
||||
"deep-diff": "1.0.2",
|
||||
"eslint": "8.26.0",
|
||||
"eslint-plugin-flowtype": "8.0.3",
|
||||
"eslint": "6.8.0",
|
||||
"eslint-plugin-flowtype": "5.1.3",
|
||||
"flow-bin": "0.119.1",
|
||||
"form-data": "4.0.0",
|
||||
"graphql-tag": "2.12.6",
|
||||
"husky": "9.0.11",
|
||||
"form-data": "3.0.0",
|
||||
"husky": "4.3.8",
|
||||
"jasmine": "3.5.0",
|
||||
"jasmine-spec-reporter": "7.0.0",
|
||||
"jsdoc": "4.0.3",
|
||||
"jsdoc": "3.6.3",
|
||||
"jsdoc-babel": "0.5.0",
|
||||
"lint-staged": "10.2.3",
|
||||
"m": "1.9.0",
|
||||
"madge": "7.0.0",
|
||||
"madge": "5.0.1",
|
||||
"mock-files-adapter": "file:spec/dependencies/mock-files-adapter",
|
||||
"mock-mail-adapter": "file:spec/dependencies/mock-mail-adapter",
|
||||
"mongodb-runner": "5.5.4",
|
||||
"node-abort-controller": "3.1.1",
|
||||
"mongodb-runner": "4.8.1",
|
||||
"mongodb-version-list": "1.0.0",
|
||||
"node-fetch": "3.2.10",
|
||||
"nyc": "15.1.0",
|
||||
"prettier": "2.0.5",
|
||||
"semantic-release": "24.0.0",
|
||||
"semantic-release": "17.4.6",
|
||||
"yaml": "1.10.0"
|
||||
},
|
||||
"scripts": {
|
||||
"ci:check": "node ./ci/ciCheck.js",
|
||||
"ci:checkNodeEngine": "node ./ci/nodeEngineCheck.js",
|
||||
"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",
|
||||
"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:runnerstart": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=$npm_config_dbversion} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} mongodb-runner start",
|
||||
"test:mongodb:testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=$npm_config_dbversion} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} 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.0.28": "npm run test:mongodb --dbversion=4.0.28",
|
||||
"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",
|
||||
"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",
|
||||
"posttest:mongodb": "mongodb-runner stop",
|
||||
"pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} mongodb-runner start",
|
||||
"testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} 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",
|
||||
"posttest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} mongodb-runner stop",
|
||||
"coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} TESTING=1 nyc jasmine",
|
||||
"start": "node ./bin/parse-server",
|
||||
"prettier": "prettier --write {src,spec}/{**/*,*}.js",
|
||||
"prepare": "npm run build",
|
||||
@@ -139,13 +133,13 @@
|
||||
"madge:circular": "node_modules/.bin/madge ./src --circular"
|
||||
},
|
||||
"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": ">=12.22.10 <19"
|
||||
},
|
||||
"bin": {
|
||||
"parse-server": "bin/parse-server"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@node-rs/bcrypt": "1.10.4"
|
||||
"@node-rs/bcrypt": "1.1.0"
|
||||
},
|
||||
"collective": {
|
||||
"type": "opencollective",
|
||||
|
||||
+46
-34
@@ -1,38 +1,50 @@
|
||||
const message = `
|
||||
1111111111
|
||||
1111111111111111
|
||||
1111111111111111111111
|
||||
11111111111111111111111111
|
||||
111111111111111 11111111
|
||||
1111111111111 111 111111
|
||||
1111111111111 111111111 111111
|
||||
111111111111 11111111111 111111
|
||||
1111111111111 11111111111 111111
|
||||
1111111111111 1111111111 111111
|
||||
1111111111111111111111111 1111111
|
||||
11111111 11111111
|
||||
111111 111 1111111111111111111
|
||||
11111 11111 111111111111111111
|
||||
11111 1 11111111111111111
|
||||
111111 111111111111111111
|
||||
11111111111111111111111111
|
||||
1111111111111111111111
|
||||
111111111111111111
|
||||
11111111111
|
||||
|
||||
Thank you for using Parse Platform!
|
||||
https://parseplatform.org
|
||||
|
||||
Please consider donating to help us maintain
|
||||
this package:
|
||||
const pkg = require('./package.json');
|
||||
|
||||
👉 https://opencollective.com/parse-server 👈
|
||||
const version = parseFloat(process.version.substr(1));
|
||||
const minimum = parseFloat(pkg.engines.node.match(/\d+/g).join('.'));
|
||||
|
||||
`;
|
||||
module.exports = function () {
|
||||
const openCollective = `
|
||||
1111111111
|
||||
1111111111111111
|
||||
1111111111111111111111
|
||||
11111111111111111111111111
|
||||
111111111111111 11111111
|
||||
1111111111111 111111
|
||||
1111111111111 111111111 111111
|
||||
111111111111 11111111111 111111
|
||||
1111111111111 11111111111 111111
|
||||
1111111111111 1111111111 111111
|
||||
1111111111111111111111111 1111111
|
||||
11111111 11111111
|
||||
111111 1111111111111111111
|
||||
11111 11111 111111111111111111
|
||||
11111 11111111111111111
|
||||
111111 111111111111111111
|
||||
11111111111111111111111111
|
||||
1111111111111111111111
|
||||
111111111111111111
|
||||
11111111111
|
||||
|
||||
function main() {
|
||||
process.stdout.write(message);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
module.exports = main;
|
||||
Thanks for installing parse 🙏
|
||||
Please consider donating to our open collective
|
||||
to help us maintain this package.
|
||||
|
||||
👉 https://opencollective.com/parse-server
|
||||
|
||||
`;
|
||||
process.stdout.write(openCollective);
|
||||
if (version >= minimum) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const errorMessage = `
|
||||
⚠️ parse-server requires at least node@${minimum}!
|
||||
You have node@${version}
|
||||
|
||||
`;
|
||||
|
||||
process.stdout.write(errorMessage);
|
||||
process.exit(1);
|
||||
};
|
||||
|
||||
@@ -2,13 +2,8 @@
|
||||
* Semantic Release Config
|
||||
*/
|
||||
|
||||
const { readFile } = require('fs').promises;
|
||||
const { resolve } = require('path');
|
||||
|
||||
// For ES6 modules use:
|
||||
// import { readFile } from 'fs/promises';
|
||||
// import { resolve, dirname } from 'path';
|
||||
// import { fileURLToPath } from 'url';
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
// Get env vars
|
||||
const ref = process.env.GITHUB_REF;
|
||||
@@ -29,11 +24,11 @@ const templates = {
|
||||
async function config() {
|
||||
|
||||
// Get branch
|
||||
const branch = ref?.split('/')?.pop()?.split('-')[0] || '(current branch could not be determined)';
|
||||
const branch = ref.split('/').pop();
|
||||
console.log(`Running on branch: ${branch}`);
|
||||
|
||||
// Set changelog file
|
||||
const changelogFile = `./changelogs/CHANGELOG_${branch}.md`;
|
||||
const changelogFile = `./changelogs/CHANGELOG_release.md`;
|
||||
console.log(`Changelog file output to: ${changelogFile}`);
|
||||
|
||||
// Load template file contents
|
||||
@@ -45,8 +40,8 @@ async function config() {
|
||||
{ name: 'alpha', prerelease: true },
|
||||
{ name: 'beta', prerelease: true },
|
||||
'next-major',
|
||||
// Long-Term-Support branch of previous major version
|
||||
'release-6.x.x',
|
||||
// Long-Term-Support branches
|
||||
{ name: 'release-5.x.x', range: '5.x.x', channel: '5.x.x' },
|
||||
],
|
||||
dryRun: false,
|
||||
debug: true,
|
||||
@@ -90,16 +85,6 @@ async function config() {
|
||||
labels: ['type:ci'],
|
||||
releasedLabels: ['state:released<%= nextRelease.channel ? `-\${nextRelease.channel}` : "" %>']
|
||||
}],
|
||||
// Back-merge module runs last because if it fails it should not impede the release process
|
||||
[
|
||||
"@saithodev/semantic-release-backmerge",
|
||||
{
|
||||
"backmergeBranches": [
|
||||
{ from: "beta", to: "alpha" },
|
||||
{ from: "release", to: "beta" },
|
||||
]
|
||||
}
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
@@ -108,17 +93,15 @@ 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));
|
||||
|
||||
const filePath = resolve(__dirname, resourcePath, templates[template].file);
|
||||
const text = await readFile(filePath, 'utf-8');
|
||||
const text = await readFile(path.resolve(__dirname, resourcePath, templates[template].file));
|
||||
templates[template].text = text;
|
||||
}
|
||||
}
|
||||
|
||||
async function readFile(filePath) {
|
||||
return await fs.readFile(filePath, 'utf-8');
|
||||
}
|
||||
|
||||
function getReleaseComment() {
|
||||
const url = repositoryUrl + '/releases/tag/${nextRelease.gitTag}';
|
||||
const comment = '🎉 This change has been released in version [${nextRelease.version}](' + url + ')';
|
||||
+9
-21
@@ -1,17 +1,10 @@
|
||||
#!/bin/sh -e
|
||||
set -x
|
||||
# GITHUB_ACTIONS=true SOURCE_TAG=test ./release_docs.sh
|
||||
|
||||
if [ "${GITHUB_ACTIONS}" = "" ];
|
||||
then
|
||||
echo "Cannot release docs without GITHUB_ACTIONS set"
|
||||
exit 0;
|
||||
fi
|
||||
if [ "${SOURCE_TAG}" = "" ];
|
||||
then
|
||||
echo "Cannot release docs without SOURCE_TAG set"
|
||||
exit 0;
|
||||
fi
|
||||
REPO="https://github.com/parse-community/parse-server"
|
||||
|
||||
rm -rf docs
|
||||
@@ -20,22 +13,17 @@ cd docs
|
||||
git pull origin gh-pages
|
||||
cd ..
|
||||
|
||||
RELEASE="release"
|
||||
VERSION="${SOURCE_TAG}"
|
||||
DEST="master"
|
||||
|
||||
# change the default page to the latest
|
||||
echo "<meta http-equiv='refresh' content='0; url=/parse-server/api/${VERSION}'>" > "docs/api/index.html"
|
||||
if [ "${SOURCE_TAG}" != "" ];
|
||||
then
|
||||
DEST="${SOURCE_TAG}"
|
||||
# change the default page to the latest
|
||||
echo "<meta http-equiv='refresh' content='0; url=/parse-server/api/${DEST}'>" > "docs/api/index.html"
|
||||
fi
|
||||
|
||||
npm run definitions
|
||||
npm run docs
|
||||
|
||||
mkdir -p "docs/api/${RELEASE}"
|
||||
cp -R out/* "docs/api/${RELEASE}"
|
||||
|
||||
mkdir -p "docs/api/${VERSION}"
|
||||
cp -R out/* "docs/api/${VERSION}"
|
||||
|
||||
# Copy other resources
|
||||
RESOURCE_DIR=".github"
|
||||
mkdir -p "docs/${RESOURCE_DIR}"
|
||||
cp "./.github/parse-server-logo.png" "docs/${RESOURCE_DIR}/"
|
||||
mkdir -p "docs/api/${DEST}"
|
||||
cp -R out/* "docs/api/${DEST}"
|
||||
|
||||
+110
-165
@@ -24,34 +24,31 @@ const nestedOptionTypes = [
|
||||
'PasswordPolicyOptions',
|
||||
'SecurityOptions',
|
||||
'SchemaOptions',
|
||||
'LogLevels',
|
||||
];
|
||||
|
||||
/** The prefix of environment variables for nested options. */
|
||||
const nestedOptionEnvPrefix = {
|
||||
AccountLockoutOptions: 'PARSE_SERVER_ACCOUNT_LOCKOUT_',
|
||||
CustomPagesOptions: 'PARSE_SERVER_CUSTOM_PAGES_',
|
||||
DatabaseOptions: 'PARSE_SERVER_DATABASE_',
|
||||
FileUploadOptions: 'PARSE_SERVER_FILE_UPLOAD_',
|
||||
IdempotencyOptions: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_',
|
||||
LiveQueryOptions: 'PARSE_SERVER_LIVEQUERY_',
|
||||
LiveQueryServerOptions: 'PARSE_LIVE_QUERY_SERVER_',
|
||||
PagesCustomUrlsOptions: 'PARSE_SERVER_PAGES_CUSTOM_URL_',
|
||||
PagesOptions: 'PARSE_SERVER_PAGES_',
|
||||
PagesRoute: 'PARSE_SERVER_PAGES_ROUTE_',
|
||||
ParseServerOptions: 'PARSE_SERVER_',
|
||||
PasswordPolicyOptions: 'PARSE_SERVER_PASSWORD_POLICY_',
|
||||
SecurityOptions: 'PARSE_SERVER_SECURITY_',
|
||||
SchemaOptions: 'PARSE_SERVER_SCHEMA_',
|
||||
LogLevels: 'PARSE_SERVER_LOG_LEVELS_',
|
||||
RateLimitOptions: 'PARSE_SERVER_RATE_LIMIT_',
|
||||
'AccountLockoutOptions': 'PARSE_SERVER_ACCOUNT_LOCKOUT_',
|
||||
'CustomPagesOptions': 'PARSE_SERVER_CUSTOM_PAGES_',
|
||||
'DatabaseOptions': 'PARSE_SERVER_DATABASE_',
|
||||
'FileUploadOptions': 'PARSE_SERVER_FILE_UPLOAD_',
|
||||
'IdempotencyOptions': 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_',
|
||||
'LiveQueryOptions': 'PARSE_SERVER_LIVEQUERY_',
|
||||
'LiveQueryServerOptions': 'PARSE_LIVE_QUERY_SERVER_',
|
||||
'PagesCustomUrlsOptions': 'PARSE_SERVER_PAGES_CUSTOM_URL_',
|
||||
'PagesOptions': 'PARSE_SERVER_PAGES_',
|
||||
'PagesRoute': 'PARSE_SERVER_PAGES_ROUTE_',
|
||||
'ParseServerOptions': 'PARSE_SERVER_',
|
||||
'PasswordPolicyOptions': 'PARSE_SERVER_PASSWORD_POLICY_',
|
||||
'SecurityOptions': 'PARSE_SERVER_SECURITY_',
|
||||
'SchemaOptions': 'PARSE_SERVER_SCHEMA_',
|
||||
};
|
||||
|
||||
function last(array) {
|
||||
return array[array.length - 1];
|
||||
}
|
||||
|
||||
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
function toENV(key) {
|
||||
let str = '';
|
||||
let previousIsUpper = false;
|
||||
@@ -71,15 +68,13 @@ function toENV(key) {
|
||||
}
|
||||
|
||||
function getCommentValue(comment) {
|
||||
if (!comment) {
|
||||
return;
|
||||
}
|
||||
if (!comment) { return }
|
||||
return comment.value.trim();
|
||||
}
|
||||
|
||||
function getENVPrefix(iface) {
|
||||
if (nestedOptionEnvPrefix[iface.id.name]) {
|
||||
return nestedOptionEnvPrefix[iface.id.name];
|
||||
return nestedOptionEnvPrefix[iface.id.name]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,11 +86,11 @@ function processProperty(property, iface) {
|
||||
if (!firstComment) {
|
||||
return;
|
||||
}
|
||||
const lines = firstComment.split('\n').map(line => line.trim());
|
||||
const lines = firstComment.split('\n').map((line) => line.trim());
|
||||
let help = '';
|
||||
let envLine;
|
||||
let defaultLine;
|
||||
lines.forEach(line => {
|
||||
lines.forEach((line) => {
|
||||
if (line.indexOf(':ENV:') === 0) {
|
||||
envLine = line;
|
||||
} else if (line.indexOf(':DEFAULT:') === 0) {
|
||||
@@ -108,13 +103,11 @@ function processProperty(property, iface) {
|
||||
if (envLine) {
|
||||
env = envLine.split(' ')[1];
|
||||
} else {
|
||||
env = prefix + toENV(name);
|
||||
env = (prefix + toENV(name));
|
||||
}
|
||||
let defaultValue;
|
||||
if (defaultLine) {
|
||||
const defaultArray = defaultLine.split(' ');
|
||||
defaultArray.shift();
|
||||
defaultValue = defaultArray.join(' ');
|
||||
defaultValue = defaultLine.split(' ')[1];
|
||||
}
|
||||
let type = property.value.type;
|
||||
let isRequired = true;
|
||||
@@ -130,20 +123,21 @@ function processProperty(property, iface) {
|
||||
defaultValue,
|
||||
types: property.value.types,
|
||||
typeAnnotation: property.value.typeAnnotation,
|
||||
required: isRequired,
|
||||
required: isRequired
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function doInterface(iface) {
|
||||
return iface.body.properties
|
||||
.sort((a, b) => a.key.name.localeCompare(b.key.name))
|
||||
.map(prop => processProperty(prop, iface))
|
||||
.filter(e => e !== undefined);
|
||||
.map((prop) => processProperty(prop, iface))
|
||||
.filter((e) => e !== undefined);
|
||||
}
|
||||
|
||||
function mapperFor(elt, t) {
|
||||
const p = t.identifier('parsers');
|
||||
const wrap = identifier => t.memberExpression(p, identifier);
|
||||
const wrap = (identifier) => t.memberExpression(p, identifier);
|
||||
|
||||
if (t.isNumberTypeAnnotation(elt)) {
|
||||
return t.callExpression(wrap(t.identifier('numberParser')), [t.stringLiteral(elt.name)]);
|
||||
@@ -161,9 +155,6 @@ function mapperFor(elt, t) {
|
||||
if (type == 'NumberOrBoolean') {
|
||||
return wrap(t.identifier('numberOrBooleanParser'));
|
||||
}
|
||||
if (type === 'StringOrStringArray') {
|
||||
return wrap(t.identifier('arrayParser'));
|
||||
}
|
||||
return wrap(t.identifier('objectParser'));
|
||||
}
|
||||
}
|
||||
@@ -180,29 +171,27 @@ function parseDefaultValue(elt, value, t) {
|
||||
literalValue = t.numericLiteral(parsers.numberOrBoolParser('')(value));
|
||||
} else if (t.isArrayTypeAnnotation(elt)) {
|
||||
const array = parsers.objectParser(value);
|
||||
literalValue = t.arrayExpression(
|
||||
array.map(value => {
|
||||
if (typeof value == 'string') {
|
||||
return t.stringLiteral(value);
|
||||
} else if (typeof value == 'number') {
|
||||
return t.numericLiteral(value);
|
||||
} else if (typeof value == 'object') {
|
||||
const object = parsers.objectParser(value);
|
||||
const props = Object.entries(object).map(([k, v]) => {
|
||||
if (typeof v == 'string') {
|
||||
return t.objectProperty(t.identifier(k), t.stringLiteral(v));
|
||||
} else if (typeof v == 'number') {
|
||||
return t.objectProperty(t.identifier(k), t.numericLiteral(v));
|
||||
} else if (typeof v == 'boolean') {
|
||||
return t.objectProperty(t.identifier(k), t.booleanLiteral(v));
|
||||
}
|
||||
});
|
||||
return t.objectExpression(props);
|
||||
} else {
|
||||
throw new Error('Unable to parse array');
|
||||
}
|
||||
})
|
||||
);
|
||||
literalValue = t.arrayExpression(array.map((value) => {
|
||||
if (typeof value == 'string') {
|
||||
return t.stringLiteral(value);
|
||||
} else if (typeof value == 'number') {
|
||||
return t.numericLiteral(value);
|
||||
} else if (typeof value == 'object') {
|
||||
const object = parsers.objectParser(value);
|
||||
const props = Object.entries(object).map(([k, v]) => {
|
||||
if (typeof v == 'string') {
|
||||
return t.objectProperty(t.identifier(k), t.stringLiteral(v));
|
||||
} else if (typeof v == 'number') {
|
||||
return t.objectProperty(t.identifier(k), t.numericLiteral(v));
|
||||
} else if (typeof v == 'boolean') {
|
||||
return t.objectProperty(t.identifier(k), t.booleanLiteral(v));
|
||||
}
|
||||
});
|
||||
return t.objectExpression(props);
|
||||
} else {
|
||||
throw new Error('Unable to parse array');
|
||||
}
|
||||
}));
|
||||
} else if (t.isAnyTypeAnnotation(elt)) {
|
||||
literalValue = t.arrayExpression([]);
|
||||
} else if (t.isBooleanTypeAnnotation(elt)) {
|
||||
@@ -215,16 +204,15 @@ function parseDefaultValue(elt, value, t) {
|
||||
|
||||
if (nestedOptionTypes.includes(type)) {
|
||||
const object = parsers.objectParser(value);
|
||||
const props = Object.keys(object).map(key => {
|
||||
const props = Object.keys(object).map((key) => {
|
||||
return t.objectProperty(key, object[value]);
|
||||
});
|
||||
literalValue = t.objectExpression(props);
|
||||
}
|
||||
if (type == 'ProtectedFields') {
|
||||
const prop = t.objectProperty(
|
||||
t.stringLiteral('_User'),
|
||||
t.objectPattern([
|
||||
t.objectProperty(t.stringLiteral('*'), t.arrayExpression([t.stringLiteral('email')])),
|
||||
t.stringLiteral('_User'), t.objectPattern([
|
||||
t.objectProperty(t.stringLiteral('*'), t.arrayExpression([t.stringLiteral('email')]))
|
||||
])
|
||||
);
|
||||
literalValue = t.objectExpression([prop]);
|
||||
@@ -235,98 +223,62 @@ function parseDefaultValue(elt, value, t) {
|
||||
|
||||
function inject(t, list) {
|
||||
let comments = '';
|
||||
const results = list
|
||||
.map(elt => {
|
||||
if (!elt.name) {
|
||||
return;
|
||||
const results = list.map((elt) => {
|
||||
if (!elt.name) {
|
||||
return;
|
||||
}
|
||||
const props = ['env', 'help'].map((key) => {
|
||||
if (elt[key]) {
|
||||
return t.objectProperty(t.stringLiteral(key), t.stringLiteral(elt[key]));
|
||||
}
|
||||
const props = ['env', 'help']
|
||||
.map(key => {
|
||||
if (elt[key]) {
|
||||
return t.objectProperty(t.stringLiteral(key), t.stringLiteral(elt[key]));
|
||||
}
|
||||
})
|
||||
.filter(e => e !== undefined);
|
||||
if (elt.required) {
|
||||
props.push(t.objectProperty(t.stringLiteral('required'), t.booleanLiteral(true)));
|
||||
}).filter((e) => e !== undefined);
|
||||
if (elt.required) {
|
||||
props.push(t.objectProperty(t.stringLiteral('required'), t.booleanLiteral(true)))
|
||||
}
|
||||
const action = mapperFor(elt, t);
|
||||
if (action) {
|
||||
props.push(t.objectProperty(t.stringLiteral('action'), action))
|
||||
}
|
||||
if (elt.defaultValue) {
|
||||
const parsedValue = parseDefaultValue(elt, elt.defaultValue, t);
|
||||
if (parsedValue) {
|
||||
props.push(t.objectProperty(t.stringLiteral('default'), parsedValue));
|
||||
} else {
|
||||
throw new Error(`Unable to parse value for ${elt.name} `);
|
||||
}
|
||||
const action = mapperFor(elt, t);
|
||||
if (action) {
|
||||
props.push(t.objectProperty(t.stringLiteral('action'), action));
|
||||
}
|
||||
|
||||
if (t.isGenericTypeAnnotation(elt)) {
|
||||
if (elt.typeAnnotation.id.name in nestedOptionEnvPrefix) {
|
||||
props.push(
|
||||
t.objectProperty(t.stringLiteral('type'), t.stringLiteral(elt.typeAnnotation.id.name))
|
||||
);
|
||||
}
|
||||
} else if (t.isArrayTypeAnnotation(elt)) {
|
||||
const elementType = elt.typeAnnotation.elementType;
|
||||
if (t.isGenericTypeAnnotation(elementType)) {
|
||||
if (elementType.id.name in nestedOptionEnvPrefix) {
|
||||
props.push(
|
||||
t.objectProperty(t.stringLiteral('type'), t.stringLiteral(elementType.id.name + '[]'))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (elt.defaultValue) {
|
||||
let parsedValue = parseDefaultValue(elt, elt.defaultValue, t);
|
||||
if (!parsedValue) {
|
||||
for (const type of elt.typeAnnotation.types) {
|
||||
elt.type = type.type;
|
||||
parsedValue = parseDefaultValue(elt, elt.defaultValue, t);
|
||||
if (parsedValue) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (parsedValue) {
|
||||
props.push(t.objectProperty(t.stringLiteral('default'), parsedValue));
|
||||
} else {
|
||||
throw new Error(`Unable to parse value for ${elt.name} `);
|
||||
}
|
||||
}
|
||||
let type = elt.type.replace('TypeAnnotation', '');
|
||||
if (type === 'Generic') {
|
||||
type = elt.typeAnnotation.id.name;
|
||||
}
|
||||
if (type === 'Array') {
|
||||
type = elt.typeAnnotation.elementType.id
|
||||
? `${elt.typeAnnotation.elementType.id.name}[]`
|
||||
: `${elt.typeAnnotation.elementType.type.replace('TypeAnnotation', '')}[]`;
|
||||
}
|
||||
if (type === 'NumberOrBoolean') {
|
||||
type = 'Number|Boolean';
|
||||
}
|
||||
if (type === 'NumberOrString') {
|
||||
type = 'Number|String';
|
||||
}
|
||||
if (type === 'Adapter') {
|
||||
const adapterType = elt.typeAnnotation.typeParameters.params[0].id.name;
|
||||
type = `Adapter<${adapterType}>`;
|
||||
}
|
||||
if (type === 'StringOrStringArray') {
|
||||
type = 'String|String[]';
|
||||
}
|
||||
comments += ` * @property {${type}} ${elt.name} ${elt.help}\n`;
|
||||
const obj = t.objectExpression(props);
|
||||
return t.objectProperty(t.stringLiteral(elt.name), obj);
|
||||
})
|
||||
.filter(elt => {
|
||||
return elt != undefined;
|
||||
});
|
||||
}
|
||||
let type = elt.type.replace('TypeAnnotation', '');
|
||||
if (type === 'Generic') {
|
||||
type = elt.typeAnnotation.id.name;
|
||||
}
|
||||
if (type === 'Array') {
|
||||
type = elt.typeAnnotation.elementType.id
|
||||
? `${elt.typeAnnotation.elementType.id.name}[]`
|
||||
: `${elt.typeAnnotation.elementType.type.replace('TypeAnnotation', '')}[]`;
|
||||
}
|
||||
if (type === 'NumberOrBoolean') {
|
||||
type = 'Number|Boolean';
|
||||
}
|
||||
if (type === 'NumberOrString') {
|
||||
type = 'Number|String';
|
||||
}
|
||||
if (type === 'Adapter') {
|
||||
const adapterType = elt.typeAnnotation.typeParameters.params[0].id.name;
|
||||
type = `Adapter<${adapterType}>`;
|
||||
}
|
||||
comments += ` * @property {${type}} ${elt.name} ${elt.help}\n`;
|
||||
const obj = t.objectExpression(props);
|
||||
return t.objectProperty(t.stringLiteral(elt.name), obj);
|
||||
}).filter((elt) => {
|
||||
return elt != undefined;
|
||||
});
|
||||
return { results, comments };
|
||||
}
|
||||
|
||||
const makeRequire = function (variableName, module, t) {
|
||||
const decl = t.variableDeclarator(
|
||||
t.identifier(variableName),
|
||||
t.callExpression(t.identifier('require'), [t.stringLiteral(module)])
|
||||
);
|
||||
return t.variableDeclaration('var', [decl]);
|
||||
};
|
||||
const decl = t.variableDeclarator(t.identifier(variableName), t.callExpression(t.identifier('require'), [t.stringLiteral(module)]));
|
||||
return t.variableDeclaration('var', [decl])
|
||||
}
|
||||
let docs = ``;
|
||||
const plugin = function (babel) {
|
||||
const t = babel.types;
|
||||
@@ -342,34 +294,27 @@ const plugin = function (babel) {
|
||||
},
|
||||
ExportDeclaration: function (path) {
|
||||
// Export declaration on an interface
|
||||
if (
|
||||
path.node &&
|
||||
path.node.declaration &&
|
||||
path.node.declaration.type == 'InterfaceDeclaration'
|
||||
) {
|
||||
if (path.node && path.node.declaration && path.node.declaration.type == 'InterfaceDeclaration') {
|
||||
const { results, comments } = inject(t, doInterface(path.node.declaration));
|
||||
const id = path.node.declaration.id.name;
|
||||
const exports = t.memberExpression(moduleExports, t.identifier(id));
|
||||
docs += `/**\n * @interface ${id}\n${comments} */\n\n`;
|
||||
path.replaceWith(t.assignmentExpression('=', exports, t.objectExpression(results)));
|
||||
path.replaceWith(
|
||||
t.assignmentExpression('=', exports, t.objectExpression(results))
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const auxiliaryCommentBefore = `
|
||||
**** GENERATED CODE ****
|
||||
This code has been generated by resources/buildConfigDefinitions.js
|
||||
Do not edit manually, but update Options/index.js
|
||||
`;
|
||||
`
|
||||
|
||||
const babel = require('@babel/core');
|
||||
const res = babel.transformFileSync('./src/Options/index.js', {
|
||||
plugins: [plugin, '@babel/transform-flow-strip-types'],
|
||||
babelrc: false,
|
||||
auxiliaryCommentBefore,
|
||||
sourceMaps: false,
|
||||
});
|
||||
const babel = require("@babel/core");
|
||||
const res = babel.transformFileSync('./src/Options/index.js', { plugins: [plugin, '@babel/transform-flow-strip-types'], babelrc: false, auxiliaryCommentBefore, sourceMaps: false });
|
||||
require('fs').writeFileSync('./src/Options/Definitions.js', res.code + '\n');
|
||||
require('fs').writeFileSync('./src/Options/docs.js', docs);
|
||||
|
||||
+3
-3
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-object-rest-spread"
|
||||
"transform-object-rest-spread"
|
||||
],
|
||||
"presets": [
|
||||
["@babel/preset-env", {
|
||||
["env", {
|
||||
"targets": {
|
||||
"node": "14"
|
||||
"node": "8"
|
||||
}
|
||||
}]
|
||||
],
|
||||
|
||||
+1
-4
@@ -15,7 +15,6 @@
|
||||
"equal": true,
|
||||
"expectAsync": true,
|
||||
"notEqual": true,
|
||||
"it_id": true,
|
||||
"it_only_db": true,
|
||||
"it_only_mongodb_version": true,
|
||||
"it_only_postgres_version": true,
|
||||
@@ -28,7 +27,6 @@
|
||||
"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,
|
||||
@@ -36,8 +34,7 @@
|
||||
"jequal": true,
|
||||
"create": true,
|
||||
"arrayContains": true,
|
||||
"databaseAdapter": true,
|
||||
"databaseURI": true
|
||||
"databaseAdapter": true
|
||||
},
|
||||
"rules": {
|
||||
"no-console": [0],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const { loadAdapter, loadModule } = require('../lib/Adapters/AdapterLoader');
|
||||
const loadAdapter = require('../lib/Adapters/AdapterLoader').loadAdapter;
|
||||
const FilesAdapter = require('@parse/fs-files-adapter').default;
|
||||
const ParsePushAdapter = require('@parse/push-adapter').default;
|
||||
const MockFilesAdapter = require('mock-files-adapter');
|
||||
const Config = require('../lib/Config');
|
||||
|
||||
@@ -102,19 +103,19 @@ describe('AdapterLoader', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should load push adapter from options', async () => {
|
||||
it('should load push adapter from options', done => {
|
||||
const options = {
|
||||
android: {
|
||||
senderId: 'yolo',
|
||||
apiKey: 'yolo',
|
||||
},
|
||||
};
|
||||
const ParsePushAdapter = await loadModule('@parse/push-adapter');
|
||||
expect(() => {
|
||||
const adapter = loadAdapter(undefined, ParsePushAdapter, options);
|
||||
expect(adapter.constructor).toBe(ParsePushAdapter);
|
||||
expect(adapter).not.toBe(undefined);
|
||||
}).not.toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('should load custom push adapter from string (#3544)', done => {
|
||||
@@ -141,25 +142,6 @@ describe('AdapterLoader', () => {
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should load custom database adapter from config', done => {
|
||||
const adapterPath = require('path').resolve('./spec/support/MockDatabaseAdapter');
|
||||
const options = {
|
||||
databaseURI: 'oracledb://user:password@localhost:1521/freepdb1',
|
||||
collectionPrefix: '',
|
||||
};
|
||||
const databaseAdapterOptions = {
|
||||
adapter: adapterPath,
|
||||
options,
|
||||
};
|
||||
expect(() => {
|
||||
const databaseAdapter = loadAdapter(databaseAdapterOptions);
|
||||
expect(databaseAdapter).not.toBe(undefined);
|
||||
expect(databaseAdapter.options).toEqual(options);
|
||||
expect(databaseAdapter.getDatabaseURI()).toEqual(options.databaseURI);
|
||||
}).not.toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('should load file adapter from direct passing', done => {
|
||||
spyOn(console, 'warn').and.callFake(() => {});
|
||||
const mockFilesAdapter = new MockFilesAdapter('key', 'secret', 'bucket');
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
const AggregateRouter = require('../lib/Routers/AggregateRouter').AggregateRouter;
|
||||
|
||||
describe('AggregateRouter', () => {
|
||||
// TODO: update pipeline syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
it('get pipeline from Array', () => {
|
||||
const body = [
|
||||
{
|
||||
$group: { _id: {} },
|
||||
group: { objectId: {} },
|
||||
},
|
||||
];
|
||||
const expected = [{ $group: { _id: {} } }];
|
||||
@@ -12,20 +13,22 @@ describe('AggregateRouter', () => {
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
// TODO: update pipeline syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
it('get pipeline from Object', () => {
|
||||
const body = {
|
||||
$group: { _id: {} },
|
||||
group: { objectId: {} },
|
||||
};
|
||||
const expected = [{ $group: { _id: {} } }];
|
||||
const result = AggregateRouter.getPipeline(body);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
// TODO: update pipeline syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
it('get pipeline from Pipeline Operator (Array)', () => {
|
||||
const body = {
|
||||
pipeline: [
|
||||
{
|
||||
$group: { _id: {} },
|
||||
group: { objectId: {} },
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -34,10 +37,11 @@ describe('AggregateRouter', () => {
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
// TODO: update pipeline syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
it('get pipeline from Pipeline Operator (Object)', () => {
|
||||
const body = {
|
||||
pipeline: {
|
||||
$group: { _id: {} },
|
||||
group: { objectId: {} },
|
||||
},
|
||||
};
|
||||
const expected = [{ $group: { _id: {} } }];
|
||||
@@ -45,42 +49,43 @@ describe('AggregateRouter', () => {
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
// TODO: update pipeline syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
it('get pipeline fails multiple keys in Array stage ', () => {
|
||||
const body = [
|
||||
{
|
||||
$group: { _id: {} },
|
||||
$match: { name: 'Test' },
|
||||
group: { objectId: {} },
|
||||
match: { name: 'Test' },
|
||||
},
|
||||
];
|
||||
expect(() => AggregateRouter.getPipeline(body)).toThrow(
|
||||
new Parse.Error(
|
||||
Parse.Error.INVALID_QUERY,
|
||||
'Pipeline stages should only have one key but found $group, $match.'
|
||||
)
|
||||
);
|
||||
try {
|
||||
AggregateRouter.getPipeline(body);
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('Pipeline stages should only have one key found group, match');
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: update pipeline syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
it('get pipeline fails multiple keys in Pipeline Operator Array stage ', () => {
|
||||
const body = {
|
||||
pipeline: [
|
||||
{
|
||||
$group: { _id: {} },
|
||||
$match: { name: 'Test' },
|
||||
group: { objectId: {} },
|
||||
match: { name: 'Test' },
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(() => AggregateRouter.getPipeline(body)).toThrow(
|
||||
new Parse.Error(
|
||||
Parse.Error.INVALID_QUERY,
|
||||
'Pipeline stages should only have one key but found $group, $match.'
|
||||
)
|
||||
);
|
||||
try {
|
||||
AggregateRouter.getPipeline(body);
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('Pipeline stages should only have one key found group, match');
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: update pipeline syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
it('get search pipeline from Pipeline Operator (Array)', () => {
|
||||
const body = {
|
||||
pipeline: {
|
||||
$search: {},
|
||||
search: {},
|
||||
},
|
||||
};
|
||||
const expected = [{ $search: {} }];
|
||||
@@ -100,7 +105,7 @@ describe('AggregateRouter', () => {
|
||||
it('support nested stage names starting with `$`', () => {
|
||||
const body = [
|
||||
{
|
||||
$lookup: {
|
||||
lookup: {
|
||||
from: 'ACollection',
|
||||
let: { id: '_id' },
|
||||
as: 'results',
|
||||
@@ -140,11 +145,11 @@ describe('AggregateRouter', () => {
|
||||
|
||||
it('support the use of `_id` in stages', () => {
|
||||
const body = [
|
||||
{ $match: { _id: 'randomId' } },
|
||||
{ $sort: { _id: -1 } },
|
||||
{ $addFields: { _id: 1 } },
|
||||
{ $group: { _id: {} } },
|
||||
{ $project: { _id: 0 } },
|
||||
{ match: { _id: 'randomId' } },
|
||||
{ sort: { _id: -1 } },
|
||||
{ addFields: { _id: 1 } },
|
||||
{ group: { _id: {} } },
|
||||
{ project: { _id: 0 } },
|
||||
];
|
||||
const expected = [
|
||||
{ $match: { _id: 'randomId' } },
|
||||
@@ -156,19 +161,4 @@ describe('AggregateRouter', () => {
|
||||
const result = AggregateRouter.getPipeline(body);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should throw with invalid stage', () => {
|
||||
expect(() => AggregateRouter.getPipeline([{ foo: 'bar' }])).toThrow(
|
||||
new Parse.Error(Parse.Error.INVALID_QUERY, `Invalid aggregate stage 'foo'.`)
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw with invalid group', () => {
|
||||
expect(() => AggregateRouter.getPipeline([{ $group: { objectId: 'bar' } }])).toThrow(
|
||||
new Parse.Error(
|
||||
Parse.Error.INVALID_QUERY,
|
||||
`Cannot use 'objectId' in aggregation stage $group.`
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -339,12 +339,11 @@ describe('AudiencesRouter', () => {
|
||||
)
|
||||
.then(result => {
|
||||
expect(result).toBeTruthy();
|
||||
|
||||
database
|
||||
.collection('test__Audience')
|
||||
.find({ _id: audience.objectId })
|
||||
.toArray()
|
||||
.then(rows => {
|
||||
.toArray((error, rows) => {
|
||||
expect(error).toEqual(undefined);
|
||||
expect(rows[0]['times_used']).toEqual(1);
|
||||
expect(rows[0]['_last_used']).toEqual(now);
|
||||
Parse._request(
|
||||
@@ -363,9 +362,6 @@ describe('AudiencesRouter', () => {
|
||||
.catch(error => {
|
||||
done.fail(error);
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
done.fail(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+32
-42
@@ -81,35 +81,46 @@ describe('Auth', () => {
|
||||
.then(roles => expect(roles).toEqual([]))
|
||||
.then(() => done());
|
||||
});
|
||||
|
||||
it('should properly handle bcrypt upgrade', done => {
|
||||
const bcryptOriginal = require('bcrypt-nodejs');
|
||||
const bcryptNew = require('bcryptjs');
|
||||
bcryptOriginal.hash('my1Long:password', null, null, function (err, res) {
|
||||
bcryptNew.compare('my1Long:password', res, function (err, res) {
|
||||
expect(res).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can use extendSessionOnUse', async () => {
|
||||
await reconfigureServer({
|
||||
extendSessionOnUse: true,
|
||||
});
|
||||
|
||||
it('should load auth without a config', async () => {
|
||||
const user = new Parse.User();
|
||||
await user.signUp({
|
||||
username: 'hello',
|
||||
password: 'password',
|
||||
});
|
||||
const session = await new Parse.Query(Parse.Session).first();
|
||||
const updatedAt = new Date('2010');
|
||||
const expiry = new Date();
|
||||
expiry.setHours(expiry.getHours() + 1);
|
||||
expect(user.getSessionToken()).not.toBeUndefined();
|
||||
const userAuth = await getAuthForSessionToken({
|
||||
sessionToken: user.getSessionToken(),
|
||||
});
|
||||
expect(userAuth.user instanceof Parse.User).toBe(true);
|
||||
expect(userAuth.user.id).toBe(user.id);
|
||||
});
|
||||
|
||||
await Parse.Server.database.update(
|
||||
'_Session',
|
||||
{ objectId: session.id },
|
||||
{
|
||||
expiresAt: { __type: 'Date', iso: expiry.toISOString() },
|
||||
updatedAt: updatedAt.toISOString(),
|
||||
}
|
||||
);
|
||||
await session.fetch();
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await session.fetch();
|
||||
expect(session.get('expiresAt') > expiry).toBeTrue();
|
||||
it('should load auth with a config', async () => {
|
||||
const user = new Parse.User();
|
||||
await user.signUp({
|
||||
username: 'hello',
|
||||
password: 'password',
|
||||
});
|
||||
expect(user.getSessionToken()).not.toBeUndefined();
|
||||
const userAuth = await getAuthForSessionToken({
|
||||
sessionToken: user.getSessionToken(),
|
||||
config: Config.get('test'),
|
||||
});
|
||||
expect(userAuth.user instanceof Parse.User).toBe(true);
|
||||
expect(userAuth.user.id).toBe(user.id);
|
||||
});
|
||||
|
||||
it('should load auth without a config', async () => {
|
||||
@@ -231,24 +242,3 @@ describe('Auth', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('extendSessionOnUse', () => {
|
||||
it(`shouldUpdateSessionExpiry()`, async () => {
|
||||
const { shouldUpdateSessionExpiry } = require('../lib/Auth');
|
||||
let update = new Date(Date.now() - 86410 * 1000);
|
||||
|
||||
const res = shouldUpdateSessionExpiry(
|
||||
{ sessionLength: 86460 },
|
||||
{ updatedAt: update }
|
||||
);
|
||||
|
||||
update = new Date(Date.now() - 43210 * 1000);
|
||||
const res2 = shouldUpdateSessionExpiry(
|
||||
{ sessionLength: 86460 },
|
||||
{ updatedAt: update }
|
||||
);
|
||||
|
||||
expect(res).toBe(true);
|
||||
expect(res2).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
+184
-455
@@ -256,49 +256,6 @@ describe('AuthenticationProviders', function () {
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('should support loginWith with session token and with/without mutated authData', async () => {
|
||||
const fakeAuthProvider = {
|
||||
validateAppId: () => Promise.resolve(),
|
||||
validateAuthData: () => Promise.resolve(),
|
||||
};
|
||||
const payload = { authData: { id: 'user1', token: 'fakeToken' } };
|
||||
const payload2 = { authData: { id: 'user1', token: 'fakeToken2' } };
|
||||
await reconfigureServer({ auth: { fakeAuthProvider } });
|
||||
const user = await Parse.User.logInWith('fakeAuthProvider', payload);
|
||||
const user2 = await Parse.User.logInWith('fakeAuthProvider', payload, {
|
||||
sessionToken: user.getSessionToken(),
|
||||
});
|
||||
const user3 = await Parse.User.logInWith('fakeAuthProvider', payload2, {
|
||||
sessionToken: user2.getSessionToken(),
|
||||
});
|
||||
expect(user.id).toEqual(user2.id);
|
||||
expect(user.id).toEqual(user3.id);
|
||||
});
|
||||
|
||||
it('should support sync/async validateAppId', async () => {
|
||||
const syncProvider = {
|
||||
validateAppId: () => true,
|
||||
appIds: 'test',
|
||||
validateAuthData: () => Promise.resolve(),
|
||||
};
|
||||
const asyncProvider = {
|
||||
appIds: 'test',
|
||||
validateAppId: () => Promise.resolve(true),
|
||||
validateAuthData: () => Promise.resolve(),
|
||||
};
|
||||
const payload = { authData: { id: 'user1', token: 'fakeToken' } };
|
||||
const syncSpy = spyOn(syncProvider, 'validateAppId');
|
||||
const asyncSpy = spyOn(asyncProvider, 'validateAppId');
|
||||
|
||||
await reconfigureServer({ auth: { asyncProvider, syncProvider } });
|
||||
const user = await Parse.User.logInWith('asyncProvider', payload);
|
||||
const user2 = await Parse.User.logInWith('syncProvider', payload);
|
||||
expect(user.getSessionToken()).toBeDefined();
|
||||
expect(user2.getSessionToken()).toBeDefined();
|
||||
expect(syncSpy).toHaveBeenCalledTimes(1);
|
||||
expect(asyncSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('unlink and link with custom provider', async () => {
|
||||
const provider = getMockMyOauthProvider();
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
@@ -382,10 +339,10 @@ describe('AuthenticationProviders', function () {
|
||||
});
|
||||
|
||||
validateAuthenticationHandler(authenticationHandler);
|
||||
const { validator } = authenticationHandler.getValidatorForProvider('customAuthentication');
|
||||
const validator = authenticationHandler.getValidatorForProvider('customAuthentication');
|
||||
validateValidator(validator);
|
||||
|
||||
validator(validAuthData, {}, {}).then(
|
||||
validator(validAuthData).then(
|
||||
() => {
|
||||
expect(authDataSpy).toHaveBeenCalled();
|
||||
// AppIds are not provided in the adapter, should not be called
|
||||
@@ -405,15 +362,12 @@ describe('AuthenticationProviders', function () {
|
||||
});
|
||||
|
||||
validateAuthenticationHandler(authenticationHandler);
|
||||
const { validator } = authenticationHandler.getValidatorForProvider('customAuthentication');
|
||||
const validator = authenticationHandler.getValidatorForProvider('customAuthentication');
|
||||
validateValidator(validator);
|
||||
validator(
|
||||
{
|
||||
token: 'my-token',
|
||||
},
|
||||
{},
|
||||
{}
|
||||
).then(
|
||||
|
||||
validator({
|
||||
token: 'my-token',
|
||||
}).then(
|
||||
() => {
|
||||
done();
|
||||
},
|
||||
@@ -433,16 +387,12 @@ describe('AuthenticationProviders', function () {
|
||||
});
|
||||
|
||||
validateAuthenticationHandler(authenticationHandler);
|
||||
const { validator } = authenticationHandler.getValidatorForProvider('customAuthentication');
|
||||
const validator = authenticationHandler.getValidatorForProvider('customAuthentication');
|
||||
validateValidator(validator);
|
||||
|
||||
validator(
|
||||
{
|
||||
token: 'valid-token',
|
||||
},
|
||||
{},
|
||||
{}
|
||||
).then(
|
||||
validator({
|
||||
token: 'valid-token',
|
||||
}).then(
|
||||
() => {
|
||||
done();
|
||||
},
|
||||
@@ -469,6 +419,29 @@ describe('AuthenticationProviders', function () {
|
||||
expect(providerOptions).toEqual(options.facebook);
|
||||
});
|
||||
|
||||
it('should throw error when Facebook request appId is wrong data type', async () => {
|
||||
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
|
||||
spyOn(httpsRequest, 'get').and.callFake(() => {
|
||||
return Promise.resolve({ id: 'a' });
|
||||
});
|
||||
const options = {
|
||||
facebook: {
|
||||
appIds: 'abcd',
|
||||
appSecret: 'secret_sauce',
|
||||
},
|
||||
};
|
||||
const authData = {
|
||||
access_token: 'badtoken',
|
||||
};
|
||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'facebook',
|
||||
options
|
||||
);
|
||||
await expectAsync(adapter.validateAppId(appIds, authData, providerOptions)).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'appIds must be an array.')
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle Facebook appSecret for validating appIds', async () => {
|
||||
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
|
||||
spyOn(httpsRequest, 'get').and.callFake(() => {
|
||||
@@ -566,6 +539,18 @@ describe('AuthenticationProviders', function () {
|
||||
new Parse.Error(Parse.Error.UNSUPPORTED_SERVICE, 'This authentication method is unsupported.')
|
||||
);
|
||||
});
|
||||
|
||||
it('can depreciate', async () => {
|
||||
const Deprecator = require('../lib/Deprecator/Deprecator');
|
||||
const spy = spyOn(Deprecator, 'logRuntimeDeprecation').and.callFake(() => {});
|
||||
const provider = getMockMyOauthProvider();
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
await Parse.User._logInWith('myoauth');
|
||||
expect(spy).toHaveBeenCalledWith({
|
||||
usage: 'auth.myoauth',
|
||||
solution: 'auth.myoauth.enabled: true',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('instagram auth adapter', () => {
|
||||
@@ -1417,6 +1402,7 @@ describe('oauth2 auth adapter', () => {
|
||||
describe('apple signin auth adapter', () => {
|
||||
const apple = require('../lib/Adapters/Auth/apple');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const util = require('util');
|
||||
const authUtils = require('../lib/Adapters/Auth/utils');
|
||||
|
||||
it('(using client id as string) should throw error with missing id_token', async () => {
|
||||
@@ -1474,10 +1460,12 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken.header);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
|
||||
const result = await apple.validateAuthData(
|
||||
{ id: 'the_user_id', token: 'the_token' },
|
||||
@@ -1489,9 +1477,11 @@ describe('apple signin auth adapter', () => {
|
||||
|
||||
it('should not verify invalid id_token', async () => {
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
|
||||
try {
|
||||
await apple.validateAuthData(
|
||||
@@ -1524,9 +1514,11 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
const result = await apple.validateAuthData(
|
||||
@@ -1544,9 +1536,11 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
const result = await apple.validateAuthData(
|
||||
@@ -1564,9 +1558,11 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
const result = await apple.validateAuthData(
|
||||
@@ -1582,9 +1578,11 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
try {
|
||||
@@ -1608,9 +1606,11 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
try {
|
||||
@@ -1629,15 +1629,17 @@ 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', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://not.apple.com',
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
try {
|
||||
@@ -1705,9 +1707,11 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'a_different_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
try {
|
||||
@@ -1969,6 +1973,7 @@ describe('microsoft graph auth adapter', () => {
|
||||
describe('facebook limited auth adapter', () => {
|
||||
const facebook = require('../lib/Adapters/Auth/facebook');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const util = require('util');
|
||||
const authUtils = require('../lib/Adapters/Auth/utils');
|
||||
|
||||
// TODO: figure out a way to run this test alongside facebook classic tests
|
||||
@@ -2024,16 +2029,23 @@ describe('facebook limited auth adapter', () => {
|
||||
|
||||
it('should use algorithm from key header to verify id_token', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://www.facebook.com',
|
||||
iss: 'https://facebook.com',
|
||||
aud: 'secret',
|
||||
exp: Date.now(),
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken.header);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
rsaPublicKey: 'the_rsa_public_key',
|
||||
};
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
|
||||
const result = await facebook.validateAuthData(
|
||||
{ id: 'the_user_id', token: 'the_token' },
|
||||
@@ -2044,10 +2056,17 @@ describe('facebook limited auth adapter', () => {
|
||||
});
|
||||
|
||||
it('should not verify invalid id_token', async () => {
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
rsaPublicKey: 'the_rsa_public_key',
|
||||
};
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
|
||||
try {
|
||||
await facebook.validateAuthData(
|
||||
@@ -2074,15 +2093,22 @@ describe('facebook limited auth adapter', () => {
|
||||
|
||||
it('(using client id as string) should verify id_token', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://www.facebook.com',
|
||||
iss: 'https://facebook.com',
|
||||
aud: 'secret',
|
||||
exp: Date.now(),
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
rsaPublicKey: 'the_rsa_public_key',
|
||||
};
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
const result = await facebook.validateAuthData(
|
||||
@@ -2094,15 +2120,22 @@ describe('facebook limited auth adapter', () => {
|
||||
|
||||
it('(using client id as array) should verify id_token', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://www.facebook.com',
|
||||
iss: 'https://facebook.com',
|
||||
aud: 'secret',
|
||||
exp: Date.now(),
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
rsaPublicKey: 'the_rsa_public_key',
|
||||
};
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
const result = await facebook.validateAuthData(
|
||||
@@ -2114,15 +2147,22 @@ describe('facebook limited auth adapter', () => {
|
||||
|
||||
it('(using client id as array with multiple items) should verify id_token', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://www.facebook.com',
|
||||
iss: 'https://facebook.com',
|
||||
aud: 'secret',
|
||||
exp: Date.now(),
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
rsaPublicKey: 'the_rsa_public_key',
|
||||
};
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
const result = await facebook.validateAuthData(
|
||||
@@ -2137,10 +2177,17 @@ describe('facebook limited auth adapter', () => {
|
||||
iss: 'https://not.facebook.com',
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
rsaPublicKey: 'the_rsa_public_key',
|
||||
};
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
try {
|
||||
@@ -2151,7 +2198,7 @@ describe('facebook limited auth adapter', () => {
|
||||
fail();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe(
|
||||
'id token not issued by correct OpenID provider - expected: https://www.facebook.com | from: https://not.facebook.com'
|
||||
'id token not issued by correct OpenID provider - expected: https://facebook.com | from: https://not.facebook.com'
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -2163,10 +2210,17 @@ describe('facebook limited auth adapter', () => {
|
||||
iss: 'https://not.facebook.com',
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
rsaPublicKey: 'the_rsa_public_key',
|
||||
};
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
try {
|
||||
@@ -2180,20 +2234,27 @@ describe('facebook limited auth adapter', () => {
|
||||
fail();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe(
|
||||
'id token not issued by correct OpenID provider - expected: https://www.facebook.com | from: https://not.facebook.com'
|
||||
'id token not issued by correct OpenID provider - expected: https://facebook.com | from: https://not.facebook.com'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('(using client id as string) with token', async () => {
|
||||
it('(using client id as string) should throw error with with invalid jwt issuer', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://not.facebook.com',
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
rsaPublicKey: 'the_rsa_public_key',
|
||||
};
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
try {
|
||||
@@ -2207,7 +2268,7 @@ describe('facebook limited auth adapter', () => {
|
||||
fail();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe(
|
||||
'id token not issued by correct OpenID provider - expected: https://www.facebook.com | from: https://not.facebook.com'
|
||||
'id token not issued by correct OpenID provider - expected: https://facebook.com | from: https://not.facebook.com'
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -2265,14 +2326,21 @@ describe('facebook limited auth adapter', () => {
|
||||
|
||||
it('should throw error with with invalid user id', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://www.facebook.com',
|
||||
iss: 'https://facebook.com',
|
||||
aud: 'invalid_client_id',
|
||||
sub: 'a_different_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
rsaPublicKey: 'the_rsa_public_key',
|
||||
};
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
try {
|
||||
@@ -2286,342 +2354,3 @@ describe('facebook limited auth adapter', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('OTP TOTP auth adatper', () => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
beforeEach(async () => {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
mfa: {
|
||||
enabled: true,
|
||||
options: ['TOTP'],
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('can enroll', async () => {
|
||||
const user = await Parse.User.signUp('username', 'password');
|
||||
const OTPAuth = require('otpauth');
|
||||
const secret = new OTPAuth.Secret();
|
||||
const totp = new OTPAuth.TOTP({
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30,
|
||||
secret,
|
||||
});
|
||||
const token = totp.generate();
|
||||
await user.save(
|
||||
{ authData: { mfa: { secret: secret.base32, token } } },
|
||||
{ sessionToken: user.getSessionToken() }
|
||||
);
|
||||
const response = user.get('authDataResponse');
|
||||
expect(response.mfa).toBeDefined();
|
||||
expect(response.mfa.recovery).toBeDefined();
|
||||
expect(response.mfa.recovery.split(',').length).toEqual(2);
|
||||
await user.fetch();
|
||||
expect(user.get('authData').mfa).toEqual({ status: 'enabled' });
|
||||
});
|
||||
|
||||
it('can login with valid token', async () => {
|
||||
const user = await Parse.User.signUp('username', 'password');
|
||||
const OTPAuth = require('otpauth');
|
||||
const secret = new OTPAuth.Secret();
|
||||
const totp = new OTPAuth.TOTP({
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30,
|
||||
secret,
|
||||
});
|
||||
const token = totp.generate();
|
||||
await user.save(
|
||||
{ authData: { mfa: { secret: secret.base32, token } } },
|
||||
{ sessionToken: user.getSessionToken() }
|
||||
);
|
||||
const response = await request({
|
||||
headers,
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/login',
|
||||
body: JSON.stringify({
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
authData: {
|
||||
mfa: {
|
||||
token: totp.generate(),
|
||||
},
|
||||
},
|
||||
}),
|
||||
}).then(res => res.data);
|
||||
expect(response.objectId).toEqual(user.id);
|
||||
expect(response.sessionToken).toBeDefined();
|
||||
expect(response.authData).toEqual({ mfa: { status: 'enabled' } });
|
||||
expect(Object.keys(response).sort()).toEqual(
|
||||
[
|
||||
'objectId',
|
||||
'username',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'authData',
|
||||
'ACL',
|
||||
'sessionToken',
|
||||
'authDataResponse',
|
||||
].sort()
|
||||
);
|
||||
});
|
||||
|
||||
it('can change OTP with valid token', async () => {
|
||||
const user = await Parse.User.signUp('username', 'password');
|
||||
const OTPAuth = require('otpauth');
|
||||
const secret = new OTPAuth.Secret();
|
||||
const totp = new OTPAuth.TOTP({
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30,
|
||||
secret,
|
||||
});
|
||||
const token = totp.generate();
|
||||
await user.save(
|
||||
{ authData: { mfa: { secret: secret.base32, token } } },
|
||||
{ sessionToken: user.getSessionToken() }
|
||||
);
|
||||
|
||||
const new_secret = new OTPAuth.Secret();
|
||||
const new_totp = new OTPAuth.TOTP({
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30,
|
||||
secret: new_secret,
|
||||
});
|
||||
const new_token = new_totp.generate();
|
||||
await user.save(
|
||||
{
|
||||
authData: { mfa: { secret: new_secret.base32, token: new_token, old: totp.generate() } },
|
||||
},
|
||||
{ sessionToken: user.getSessionToken() }
|
||||
);
|
||||
await user.fetch({ useMasterKey: true });
|
||||
expect(user.get('authData').mfa.secret).toEqual(new_secret.base32);
|
||||
});
|
||||
|
||||
it('cannot change OTP with invalid token', async () => {
|
||||
const user = await Parse.User.signUp('username', 'password');
|
||||
const OTPAuth = require('otpauth');
|
||||
const secret = new OTPAuth.Secret();
|
||||
const totp = new OTPAuth.TOTP({
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30,
|
||||
secret,
|
||||
});
|
||||
const token = totp.generate();
|
||||
await user.save(
|
||||
{ authData: { mfa: { secret: secret.base32, token } } },
|
||||
{ sessionToken: user.getSessionToken() }
|
||||
);
|
||||
|
||||
const new_secret = new OTPAuth.Secret();
|
||||
const new_totp = new OTPAuth.TOTP({
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30,
|
||||
secret: new_secret,
|
||||
});
|
||||
const new_token = new_totp.generate();
|
||||
await expectAsync(
|
||||
user.save(
|
||||
{
|
||||
authData: { mfa: { secret: new_secret.base32, token: new_token, old: '123' } },
|
||||
},
|
||||
{ sessionToken: user.getSessionToken() }
|
||||
)
|
||||
).toBeRejectedWith(new Parse.Error(Parse.Error.OTHER_CAUSE, 'Invalid MFA token'));
|
||||
await user.fetch({ useMasterKey: true });
|
||||
expect(user.get('authData').mfa.secret).toEqual(secret.base32);
|
||||
});
|
||||
|
||||
it('future logins require TOTP token', async () => {
|
||||
const user = await Parse.User.signUp('username', 'password');
|
||||
const OTPAuth = require('otpauth');
|
||||
const secret = new OTPAuth.Secret();
|
||||
const totp = new OTPAuth.TOTP({
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30,
|
||||
secret,
|
||||
});
|
||||
const token = totp.generate();
|
||||
await user.save(
|
||||
{ authData: { mfa: { secret: secret.base32, token } } },
|
||||
{ sessionToken: user.getSessionToken() }
|
||||
);
|
||||
await expectAsync(Parse.User.logIn('username', 'password')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OTHER_CAUSE, 'Missing additional authData mfa')
|
||||
);
|
||||
});
|
||||
|
||||
it('future logins reject incorrect TOTP token', async () => {
|
||||
const user = await Parse.User.signUp('username', 'password');
|
||||
const OTPAuth = require('otpauth');
|
||||
const secret = new OTPAuth.Secret();
|
||||
const totp = new OTPAuth.TOTP({
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30,
|
||||
secret,
|
||||
});
|
||||
const token = totp.generate();
|
||||
await user.save(
|
||||
{ authData: { mfa: { secret: secret.base32, token } } },
|
||||
{ sessionToken: user.getSessionToken() }
|
||||
);
|
||||
await expectAsync(
|
||||
request({
|
||||
headers,
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/login',
|
||||
body: JSON.stringify({
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
authData: {
|
||||
mfa: {
|
||||
token: 'abcd',
|
||||
},
|
||||
},
|
||||
}),
|
||||
}).catch(e => {
|
||||
throw e.data;
|
||||
})
|
||||
).toBeRejectedWith({ code: Parse.Error.SCRIPT_FAILED, error: 'Invalid MFA token' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('OTP SMS auth adatper', () => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
let code;
|
||||
let mobile;
|
||||
const mfa = {
|
||||
enabled: true,
|
||||
options: ['SMS'],
|
||||
sendSMS(smsCode, number) {
|
||||
expect(smsCode).toBeDefined();
|
||||
expect(number).toBeDefined();
|
||||
expect(smsCode.length).toEqual(6);
|
||||
code = smsCode;
|
||||
mobile = number;
|
||||
},
|
||||
digits: 6,
|
||||
period: 30,
|
||||
};
|
||||
beforeEach(async () => {
|
||||
code = '';
|
||||
mobile = '';
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
mfa,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('can enroll', async () => {
|
||||
const user = await Parse.User.signUp('username', 'password');
|
||||
const sessionToken = user.getSessionToken();
|
||||
const spy = spyOn(mfa, 'sendSMS').and.callThrough();
|
||||
await user.save({ authData: { mfa: { mobile: '+11111111111' } } }, { sessionToken });
|
||||
await user.fetch({ sessionToken });
|
||||
expect(user.get('authData')).toEqual({ mfa: { status: 'disabled' } });
|
||||
expect(spy).toHaveBeenCalledWith(code, '+11111111111');
|
||||
await user.fetch({ useMasterKey: true });
|
||||
const authData = user.get('authData').mfa?.pending;
|
||||
expect(authData).toBeDefined();
|
||||
expect(authData['+11111111111']).toBeDefined();
|
||||
expect(Object.keys(authData['+11111111111'])).toEqual(['token', 'expiry']);
|
||||
|
||||
await user.save({ authData: { mfa: { mobile, token: code } } }, { sessionToken });
|
||||
await user.fetch({ sessionToken });
|
||||
expect(user.get('authData')).toEqual({ mfa: { status: 'enabled' } });
|
||||
});
|
||||
|
||||
it('future logins require SMS code', async () => {
|
||||
const user = await Parse.User.signUp('username', 'password');
|
||||
const spy = spyOn(mfa, 'sendSMS').and.callThrough();
|
||||
await user.save(
|
||||
{ authData: { mfa: { mobile: '+11111111111' } } },
|
||||
{ sessionToken: user.getSessionToken() }
|
||||
);
|
||||
|
||||
await user.save(
|
||||
{ authData: { mfa: { mobile, token: code } } },
|
||||
{ sessionToken: user.getSessionToken() }
|
||||
);
|
||||
|
||||
spy.calls.reset();
|
||||
|
||||
await expectAsync(Parse.User.logIn('username', 'password')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OTHER_CAUSE, 'Missing additional authData mfa')
|
||||
);
|
||||
const res = await request({
|
||||
headers,
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/login',
|
||||
body: JSON.stringify({
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
authData: {
|
||||
mfa: {
|
||||
token: 'request',
|
||||
},
|
||||
},
|
||||
}),
|
||||
}).catch(e => e.data);
|
||||
expect(res).toEqual({ code: Parse.Error.SCRIPT_FAILED, error: 'Please enter the token' });
|
||||
expect(spy).toHaveBeenCalledWith(code, '+11111111111');
|
||||
const response = await request({
|
||||
headers,
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/login',
|
||||
body: JSON.stringify({
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
authData: {
|
||||
mfa: {
|
||||
token: code,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}).then(res => res.data);
|
||||
expect(response.objectId).toEqual(user.id);
|
||||
expect(response.sessionToken).toBeDefined();
|
||||
expect(response.authData).toEqual({ mfa: { status: 'enabled' } });
|
||||
expect(Object.keys(response).sort()).toEqual(
|
||||
[
|
||||
'objectId',
|
||||
'username',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'authData',
|
||||
'ACL',
|
||||
'sessionToken',
|
||||
'authDataResponse',
|
||||
].sort()
|
||||
);
|
||||
});
|
||||
|
||||
it('partially enrolled users can still login', async () => {
|
||||
const user = await Parse.User.signUp('username', 'password');
|
||||
await user.save({ authData: { mfa: { mobile: '+11111111111' } } });
|
||||
const spy = spyOn(mfa, 'sendSMS').and.callThrough();
|
||||
await Parse.User.logIn('username', 'password');
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+70
-103
@@ -74,7 +74,7 @@ describe('commander additions', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should load properly use args over env', () => {
|
||||
it('should load properly use args over env', done => {
|
||||
commander.loadDefinitions(testDefinitions);
|
||||
commander.parse(['node', './CLI.spec.js', '--arg0', 'arg0Value', '--arg4', ''], {
|
||||
PROGRAM_ARG_0: 'arg0ENVValue',
|
||||
@@ -86,6 +86,7 @@ describe('commander additions', () => {
|
||||
expect(commander.arg1).toEqual('arg1ENVValue');
|
||||
expect(commander.arg2).toEqual(4);
|
||||
expect(commander.arg4).toEqual('');
|
||||
done();
|
||||
});
|
||||
|
||||
it('should fail in action as port is invalid', done => {
|
||||
@@ -207,36 +208,6 @@ describe('LiveQuery definitions', () => {
|
||||
describe('execution', () => {
|
||||
const binPath = path.resolve(__dirname, '../bin/parse-server');
|
||||
let childProcess;
|
||||
let aggregatedData;
|
||||
|
||||
function handleStdout(childProcess, done, aggregatedData, requiredData) {
|
||||
childProcess.stdout.on('data', data => {
|
||||
data = data.toString();
|
||||
aggregatedData.push(data);
|
||||
if (requiredData.every(required => aggregatedData.some(aggregated => aggregated.includes(required)))) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleStderr(childProcess, done) {
|
||||
childProcess.stderr.on('data', data => {
|
||||
data = data.toString();
|
||||
if (!data.includes('[DEP0040] DeprecationWarning')) {
|
||||
done.fail(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleError(childProcess, done) {
|
||||
childProcess.on('error', err => {
|
||||
done.fail(err);
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
aggregatedData = [];
|
||||
});
|
||||
|
||||
afterEach(done => {
|
||||
if (childProcess) {
|
||||
@@ -248,83 +219,79 @@ describe('execution', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should start Parse Server', done => {
|
||||
const env = { ...process.env };
|
||||
env.NODE_OPTIONS = '--dns-result-order=ipv4first --trace-deprecation';
|
||||
childProcess = spawn(
|
||||
binPath,
|
||||
['--appId', 'test', '--masterKey', 'test', '--databaseURI', databaseURI, '--port', '1339'],
|
||||
{ env }
|
||||
);
|
||||
handleStdout(childProcess, done, aggregatedData, ['parse-server running on']);
|
||||
handleStderr(childProcess, done);
|
||||
handleError(childProcess, done);
|
||||
});
|
||||
|
||||
it('should start Parse Server with GraphQL', async done => {
|
||||
const env = { ...process.env };
|
||||
env.NODE_OPTIONS = '--dns-result-order=ipv4first --trace-deprecation';
|
||||
childProcess = spawn(
|
||||
binPath,
|
||||
[
|
||||
'--appId',
|
||||
'test',
|
||||
'--masterKey',
|
||||
'test',
|
||||
'--databaseURI',
|
||||
databaseURI,
|
||||
'--port',
|
||||
'1340',
|
||||
'--mountGraphQL',
|
||||
],
|
||||
{ env }
|
||||
);
|
||||
handleStdout(childProcess, done, aggregatedData, [
|
||||
'parse-server running on',
|
||||
'GraphQL running on',
|
||||
it('shoud start Parse Server', done => {
|
||||
childProcess = spawn(binPath, [
|
||||
'--appId',
|
||||
'test',
|
||||
'--masterKey',
|
||||
'test',
|
||||
'--databaseURI',
|
||||
'mongodb://localhost/test',
|
||||
'--port',
|
||||
'1339',
|
||||
]);
|
||||
handleStderr(childProcess, done);
|
||||
handleError(childProcess, done);
|
||||
childProcess.stdout.on('data', data => {
|
||||
data = data.toString();
|
||||
if (data.includes('parse-server running on')) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
childProcess.stderr.on('data', data => {
|
||||
done.fail(data.toString());
|
||||
});
|
||||
});
|
||||
|
||||
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(
|
||||
binPath,
|
||||
[
|
||||
'--appId',
|
||||
'test',
|
||||
'--masterKey',
|
||||
'test',
|
||||
'--databaseURI',
|
||||
databaseURI,
|
||||
'--port',
|
||||
'1341',
|
||||
'--mountGraphQL',
|
||||
'--mountPlayground',
|
||||
],
|
||||
{ env }
|
||||
);
|
||||
handleStdout(childProcess, done, aggregatedData, [
|
||||
'parse-server running on',
|
||||
'Playground running on',
|
||||
'GraphQL running on',
|
||||
it('shoud start Parse Server with GraphQL', done => {
|
||||
childProcess = spawn(binPath, [
|
||||
'--appId',
|
||||
'test',
|
||||
'--masterKey',
|
||||
'test',
|
||||
'--databaseURI',
|
||||
'mongodb://localhost/test',
|
||||
'--port',
|
||||
'1340',
|
||||
'--mountGraphQL',
|
||||
]);
|
||||
handleStderr(childProcess, done);
|
||||
handleError(childProcess, done);
|
||||
let output = '';
|
||||
childProcess.stdout.on('data', data => {
|
||||
data = data.toString();
|
||||
output += data;
|
||||
if (data.includes('GraphQL running on')) {
|
||||
expect(output).toMatch('parse-server running on');
|
||||
done();
|
||||
}
|
||||
});
|
||||
childProcess.stderr.on('data', data => {
|
||||
done.fail(data.toString());
|
||||
});
|
||||
});
|
||||
|
||||
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(
|
||||
binPath,
|
||||
['--databaseURI', databaseURI, './spec/configs/CLIConfigAuth.json'],
|
||||
{ env }
|
||||
);
|
||||
handleStdout(childProcess, done, aggregatedData, ['parse-server running on']);
|
||||
handleStderr(childProcess, done);
|
||||
handleError(childProcess, done);
|
||||
it('shoud start Parse Server with GraphQL and Playground', done => {
|
||||
childProcess = spawn(binPath, [
|
||||
'--appId',
|
||||
'test',
|
||||
'--masterKey',
|
||||
'test',
|
||||
'--databaseURI',
|
||||
'mongodb://localhost/test',
|
||||
'--port',
|
||||
'1341',
|
||||
'--mountGraphQL',
|
||||
'--mountPlayground',
|
||||
]);
|
||||
let output = '';
|
||||
childProcess.stdout.on('data', data => {
|
||||
data = data.toString();
|
||||
output += data;
|
||||
if (data.includes('Playground running on')) {
|
||||
expect(output).toMatch('GraphQL running on');
|
||||
expect(output).toMatch('parse-server running on');
|
||||
done();
|
||||
}
|
||||
});
|
||||
childProcess.stderr.on('data', data => {
|
||||
done.fail(data.toString());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -194,6 +194,27 @@ describe('cloud validator', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('set params on cloud functions', done => {
|
||||
Parse.Cloud.define(
|
||||
'hello',
|
||||
() => {
|
||||
return 'Hello world!';
|
||||
},
|
||||
{
|
||||
fields: ['a'],
|
||||
}
|
||||
);
|
||||
Parse.Cloud.run('hello', {})
|
||||
.then(() => {
|
||||
fail('function should have failed.');
|
||||
})
|
||||
.catch(error => {
|
||||
expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR);
|
||||
expect(error.message).toEqual('Validation failed. Please specify data for a.');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('allow params on cloud functions', done => {
|
||||
Parse.Cloud.define(
|
||||
'hello',
|
||||
@@ -1608,7 +1629,7 @@ describe('cloud validator', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('Logs on multiple invalid configs', () => {
|
||||
it('Logs on invalid config', () => {
|
||||
const fields = [
|
||||
{
|
||||
field: 'otherKey',
|
||||
|
||||
+88
-264
@@ -1,7 +1,6 @@
|
||||
'use strict';
|
||||
const Config = require('../lib/Config');
|
||||
const Parse = require('parse/node');
|
||||
const ParseServer = require('../lib/index').ParseServer;
|
||||
const request = require('../lib/request');
|
||||
const InMemoryCacheAdapter = require('../lib/Adapters/Cache/InMemoryCacheAdapter')
|
||||
.InMemoryCacheAdapter;
|
||||
@@ -40,48 +39,6 @@ describe('Cloud Code', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('can load cloud code as a module', async () => {
|
||||
process.env.npm_package_type = 'module';
|
||||
await reconfigureServer({ appId: 'test1', cloud: './spec/cloud/cloudCodeModuleFile.js' });
|
||||
const result = await Parse.Cloud.run('cloudCodeInFile');
|
||||
expect(result).toEqual('It is possible to define cloud code in a file.');
|
||||
delete process.env.npm_package_type;
|
||||
});
|
||||
|
||||
it('cloud code must be valid type', async () => {
|
||||
spyOn(console, 'error').and.callFake(() => {});
|
||||
await expectAsync(reconfigureServer({ cloud: true })).toBeRejectedWith(
|
||||
"argument 'cloud' must either be a string or a function"
|
||||
);
|
||||
});
|
||||
|
||||
it('should wait for cloud code to load', async () => {
|
||||
await reconfigureServer({ appId: 'test3' });
|
||||
const initiated = new Date();
|
||||
const parseServer = await new ParseServer({
|
||||
...defaultConfiguration,
|
||||
appId: 'test3',
|
||||
masterKey: 'test',
|
||||
serverURL: 'http://localhost:12668/parse',
|
||||
async cloud() {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
Parse.Cloud.beforeSave('Test', () => {
|
||||
throw 'Cannot save.';
|
||||
});
|
||||
},
|
||||
}).start();
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
app.use('/parse', parseServer.app);
|
||||
const server = app.listen(12668);
|
||||
const now = new Date();
|
||||
expect(now.getTime() - initiated.getTime() > 1000).toBeTrue();
|
||||
await expectAsync(new Parse.Object('Test').save()).toBeRejectedWith(
|
||||
new Parse.Error(141, 'Cannot save.')
|
||||
);
|
||||
await new Promise(resolve => server.close(resolve));
|
||||
});
|
||||
|
||||
it('can create functions', done => {
|
||||
Parse.Cloud.define('hello', () => {
|
||||
return 'Hello world!';
|
||||
@@ -96,22 +53,13 @@ describe('Cloud Code', () => {
|
||||
it('can get config', () => {
|
||||
const config = Parse.Server;
|
||||
let currentConfig = Config.get('test');
|
||||
const server = require('../lib/cloud-code/Parse.Server');
|
||||
expect(Object.keys(config)).toEqual(Object.keys({ ...currentConfig, ...server }));
|
||||
expect(Object.keys(config)).toEqual(Object.keys(currentConfig));
|
||||
config.silent = false;
|
||||
Parse.Server = config;
|
||||
currentConfig = Config.get('test');
|
||||
expect(currentConfig.silent).toBeFalse();
|
||||
});
|
||||
|
||||
it('can get curent version', () => {
|
||||
const version = require('../package.json').version;
|
||||
const currentConfig = Config.get('test');
|
||||
expect(Parse.Server.version).toBeDefined();
|
||||
expect(currentConfig.version).toBeDefined();
|
||||
expect(Parse.Server.version).toEqual(version);
|
||||
});
|
||||
|
||||
it('show warning on duplicate cloud functions', done => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
spyOn(logger, 'warn').and.callFake(() => {});
|
||||
@@ -1362,48 +1310,6 @@ describe('Cloud Code', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should not encode Parse Objects', async () => {
|
||||
await reconfigureServer({ encodeParseObjectInCloudFunction: false });
|
||||
const user = new Parse.User();
|
||||
user.setUsername('username');
|
||||
user.setPassword('password');
|
||||
user.set('deleted', false);
|
||||
await user.signUp();
|
||||
Parse.Cloud.define(
|
||||
'deleteAccount',
|
||||
async req => {
|
||||
expect(req.params.object instanceof Parse.Object).not.toBeTrue();
|
||||
return 'Object deleted';
|
||||
},
|
||||
{
|
||||
requireMaster: true,
|
||||
}
|
||||
);
|
||||
await Parse.Cloud.run('deleteAccount', { object: user.toPointer() }, { useMasterKey: true });
|
||||
});
|
||||
|
||||
it('allow cloud to encode Parse Objects', async () => {
|
||||
await reconfigureServer({ encodeParseObjectInCloudFunction: true });
|
||||
const user = new Parse.User();
|
||||
user.setUsername('username');
|
||||
user.setPassword('password');
|
||||
user.set('deleted', false);
|
||||
await user.signUp();
|
||||
Parse.Cloud.define(
|
||||
'deleteAccount',
|
||||
async req => {
|
||||
expect(req.params.object instanceof Parse.Object).toBeTrue();
|
||||
req.params.object.set('deleted', true);
|
||||
await req.params.object.save(null, { useMasterKey: true });
|
||||
return 'Object deleted';
|
||||
},
|
||||
{
|
||||
requireMaster: true,
|
||||
}
|
||||
);
|
||||
await Parse.Cloud.run('deleteAccount', { object: user.toPointer() }, { useMasterKey: true });
|
||||
});
|
||||
|
||||
it('beforeSave should not affect fetched pointers', done => {
|
||||
Parse.Cloud.beforeSave('BeforeSaveUnchanged', () => {});
|
||||
|
||||
@@ -1652,22 +1558,6 @@ describe('Cloud Code', () => {
|
||||
expect(obj.get('foo')).toBe('bar');
|
||||
});
|
||||
|
||||
it('create role with name and ACL and a beforeSave', async () => {
|
||||
Parse.Cloud.beforeSave(Parse.Role, ({ object }) => {
|
||||
return object;
|
||||
});
|
||||
|
||||
const obj = new Parse.Role('TestRole', new Parse.ACL({ '*': { read: true, write: true } }));
|
||||
await obj.save();
|
||||
|
||||
expect(obj.getACL()).toEqual(new Parse.ACL({ '*': { read: true, write: true } }));
|
||||
expect(obj.get('name')).toEqual('TestRole');
|
||||
await obj.fetch();
|
||||
|
||||
expect(obj.getACL()).toEqual(new Parse.ACL({ '*': { read: true, write: true } }));
|
||||
expect(obj.get('name')).toEqual('TestRole');
|
||||
});
|
||||
|
||||
it('can unset in afterSave', async () => {
|
||||
Parse.Cloud.beforeSave('TestObject', ({ object }) => {
|
||||
if (!object.existed()) {
|
||||
@@ -1796,6 +1686,25 @@ describe('Cloud Code', () => {
|
||||
obj.save().then(done, done.fail);
|
||||
});
|
||||
|
||||
it('can deprecate Parse.Cloud.httpRequest', async () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
spyOn(logger, 'warn').and.callFake(() => {});
|
||||
Parse.Cloud.define('hello', () => {
|
||||
return 'Hello world!';
|
||||
});
|
||||
await Parse.Cloud.httpRequest({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/functions/hello',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
});
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
'DeprecationWarning: Parse.Cloud.httpRequest is deprecated and will be removed in a future version. Use a http request library instead.'
|
||||
);
|
||||
});
|
||||
|
||||
describe('cloud jobs', () => {
|
||||
it('should define a job', done => {
|
||||
expect(() => {
|
||||
@@ -2400,56 +2309,6 @@ describe('beforeFind hooks', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('sets correct beforeFind trigger isGet parameter for Parse.Object.fetch request', async () => {
|
||||
const hook = {
|
||||
method: req => {
|
||||
expect(req.isGet).toEqual(true);
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
spyOn(hook, 'method').and.callThrough();
|
||||
Parse.Cloud.beforeFind('MyObject', hook.method);
|
||||
const obj = new Parse.Object('MyObject');
|
||||
await obj.save();
|
||||
const getObj = await obj.fetch();
|
||||
expect(getObj).toBeInstanceOf(Parse.Object);
|
||||
expect(hook.method).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('sets correct beforeFind trigger isGet parameter for Parse.Query.get request', async () => {
|
||||
const hook = {
|
||||
method: req => {
|
||||
expect(req.isGet).toEqual(false);
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
spyOn(hook, 'method').and.callThrough();
|
||||
Parse.Cloud.beforeFind('MyObject', hook.method);
|
||||
const obj = new Parse.Object('MyObject');
|
||||
await obj.save();
|
||||
const query = new Parse.Query('MyObject');
|
||||
const getObj = await query.get(obj.id);
|
||||
expect(getObj).toBeInstanceOf(Parse.Object);
|
||||
expect(hook.method).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('sets correct beforeFind trigger isGet parameter for Parse.Query.find request', async () => {
|
||||
const hook = {
|
||||
method: req => {
|
||||
expect(req.isGet).toEqual(false);
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
spyOn(hook, 'method').and.callThrough();
|
||||
Parse.Cloud.beforeFind('MyObject', hook.method);
|
||||
const obj = new Parse.Object('MyObject');
|
||||
await obj.save();
|
||||
const query = new Parse.Query('MyObject');
|
||||
const findObjs = await query.find();
|
||||
expect(findObjs?.[0]).toBeInstanceOf(Parse.Object);
|
||||
expect(hook.method).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should have request headers', done => {
|
||||
Parse.Cloud.beforeFind('MyObject', req => {
|
||||
expect(req.headers).toBeDefined();
|
||||
@@ -2483,60 +2342,6 @@ describe('beforeFind hooks', () => {
|
||||
})
|
||||
.then(() => done());
|
||||
});
|
||||
|
||||
it('should run beforeFind on pointers and array of pointers from an object', async () => {
|
||||
const obj1 = new Parse.Object('TestObject');
|
||||
const obj2 = new Parse.Object('TestObject2');
|
||||
const obj3 = new Parse.Object('TestObject');
|
||||
obj2.set('aField', 'aFieldValue');
|
||||
await obj2.save();
|
||||
obj1.set('pointerField', obj2);
|
||||
obj3.set('pointerFieldArray', [obj2]);
|
||||
await obj1.save();
|
||||
await obj3.save();
|
||||
const spy = jasmine.createSpy('beforeFindSpy');
|
||||
Parse.Cloud.beforeFind('TestObject2', spy);
|
||||
const query = new Parse.Query('TestObject');
|
||||
await query.get(obj1.id);
|
||||
// Pointer not included in query so we don't expect beforeFind to be called
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
const query2 = new Parse.Query('TestObject');
|
||||
query2.include('pointerField');
|
||||
const res = await query2.get(obj1.id);
|
||||
expect(res.get('pointerField').get('aField')).toBe('aFieldValue');
|
||||
// Pointer included in query so we expect beforeFind to be called
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
const query3 = new Parse.Query('TestObject');
|
||||
query3.include('pointerFieldArray');
|
||||
const res2 = await query3.get(obj3.id);
|
||||
expect(res2.get('pointerFieldArray')[0].get('aField')).toBe('aFieldValue');
|
||||
expect(spy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should have access to context in include query in beforeFind hook', async () => {
|
||||
let beforeFindTestObjectCalled = false;
|
||||
let beforeFindTestObject2Called = false;
|
||||
const obj1 = new Parse.Object('TestObject');
|
||||
const obj2 = new Parse.Object('TestObject2');
|
||||
obj2.set('aField', 'aFieldValue');
|
||||
await obj2.save();
|
||||
obj1.set('pointerField', obj2);
|
||||
await obj1.save();
|
||||
Parse.Cloud.beforeFind('TestObject', req => {
|
||||
expect(req.context).toBeDefined();
|
||||
expect(req.context.a).toEqual('a');
|
||||
beforeFindTestObjectCalled = true;
|
||||
});
|
||||
Parse.Cloud.beforeFind('TestObject2', req => {
|
||||
expect(req.context).toBeDefined();
|
||||
expect(req.context.a).toEqual('a');
|
||||
beforeFindTestObject2Called = true;
|
||||
});
|
||||
const query = new Parse.Query('TestObject');
|
||||
await query.include('pointerField').find({ context: { a: 'a' } });
|
||||
expect(beforeFindTestObjectCalled).toBeTrue();
|
||||
expect(beforeFindTestObject2Called).toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
describe('afterFind hooks', () => {
|
||||
@@ -2930,7 +2735,7 @@ describe('afterFind hooks', () => {
|
||||
const obj = new Parse.Object('MyObject');
|
||||
const pipeline = [
|
||||
{
|
||||
$group: { _id: {} },
|
||||
group: { objectId: {} },
|
||||
},
|
||||
];
|
||||
obj
|
||||
@@ -3317,36 +3122,6 @@ describe('beforeLogin hook', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('does not crash server when throwing in afterLogin hook', async () => {
|
||||
const error = new Parse.Error(2000, 'afterLogin error');
|
||||
const trigger = {
|
||||
afterLogin() {
|
||||
throw error;
|
||||
},
|
||||
};
|
||||
const spy = spyOn(trigger, 'afterLogin').and.callThrough();
|
||||
Parse.Cloud.afterLogin(trigger.afterLogin);
|
||||
await Parse.User.signUp('user', 'pass');
|
||||
const response = await Parse.User.logIn('user', 'pass').catch(e => e);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(response).toEqual(error);
|
||||
});
|
||||
|
||||
it('does not crash server when throwing in afterLogout hook', async () => {
|
||||
const error = new Parse.Error(2000, 'afterLogout error');
|
||||
const trigger = {
|
||||
afterLogout() {
|
||||
throw error;
|
||||
},
|
||||
};
|
||||
const spy = spyOn(trigger, 'afterLogout').and.callThrough();
|
||||
Parse.Cloud.afterLogout(trigger.afterLogout);
|
||||
await Parse.User.signUp('user', 'pass');
|
||||
const response = await Parse.User.logOut().catch(e => e);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(response).toEqual(error);
|
||||
});
|
||||
|
||||
it('should have expected data in request', async done => {
|
||||
Parse.Cloud.beforeLogin(req => {
|
||||
expect(req.object).toBeDefined();
|
||||
@@ -3354,7 +3129,7 @@ describe('beforeLogin hook', () => {
|
||||
expect(req.headers).toBeDefined();
|
||||
expect(req.ip).toBeDefined();
|
||||
expect(req.installationId).toBeDefined();
|
||||
expect(req.context).toBeDefined();
|
||||
expect(req.context).toBeUndefined();
|
||||
});
|
||||
|
||||
await Parse.User.signUp('tupac', 'shakur');
|
||||
@@ -3471,7 +3246,7 @@ describe('afterLogin hook', () => {
|
||||
expect(req.headers).toBeDefined();
|
||||
expect(req.ip).toBeDefined();
|
||||
expect(req.installationId).toBeDefined();
|
||||
expect(req.context).toBeDefined();
|
||||
expect(req.context).toBeUndefined();
|
||||
});
|
||||
|
||||
await Parse.User.signUp('testuser', 'p@ssword');
|
||||
@@ -3637,7 +3412,7 @@ describe('afterLogin hook', () => {
|
||||
});
|
||||
|
||||
describe('saveFile hooks', () => {
|
||||
it('beforeSave(Parse.File) should return file that is already saved and not save anything to files adapter', async () => {
|
||||
it('beforeSaveFile should return file that is already saved and not save anything to files adapter', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough();
|
||||
Parse.Cloud.beforeSave(Parse.File, () => {
|
||||
@@ -3653,7 +3428,7 @@ describe('saveFile hooks', () => {
|
||||
expect(createFileSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('beforeSave(Parse.File) should throw error', async () => {
|
||||
it('beforeSaveFile should throw error', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
Parse.Cloud.beforeSave(Parse.File, () => {
|
||||
throw new Parse.Error(400, 'some-error-message');
|
||||
@@ -3666,7 +3441,7 @@ describe('saveFile hooks', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('beforeSave(Parse.File) should change values of uploaded file by editing fileObject directly', async () => {
|
||||
it('beforeSaveFile should change values of uploaded file by editing fileObject directly', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough();
|
||||
Parse.Cloud.beforeSave(Parse.File, async req => {
|
||||
@@ -3695,7 +3470,7 @@ describe('saveFile hooks', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('beforeSave(Parse.File) should change values by returning new fileObject', async () => {
|
||||
it('beforeSaveFile should change values by returning new fileObject', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough();
|
||||
Parse.Cloud.beforeSave(Parse.File, async req => {
|
||||
@@ -3729,7 +3504,7 @@ describe('saveFile hooks', () => {
|
||||
expect(file._name.indexOf(expectedFileName)).toBe(file._name.length - expectedFileName.length);
|
||||
});
|
||||
|
||||
it('beforeSave(Parse.File) should contain metadata and tags saved from client', async () => {
|
||||
it('beforeSaveFile should contain metadata and tags saved from client', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough();
|
||||
Parse.Cloud.beforeSave(Parse.File, async req => {
|
||||
@@ -3757,7 +3532,7 @@ describe('saveFile hooks', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('beforeSave(Parse.File) should return same file data with new file name', async () => {
|
||||
it('beforeSaveFile should return same file data with new file name', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
const config = Config.get('test');
|
||||
config.filesController.options.preserveFileName = true;
|
||||
@@ -3772,7 +3547,7 @@ describe('saveFile hooks', () => {
|
||||
expect(result.name()).toBe('2020-04-01.txt');
|
||||
});
|
||||
|
||||
it('afterSave(Parse.File) should set fileSize to null if beforeSave returns an already saved file', async () => {
|
||||
it('afterSaveFile should set fileSize to null if beforeSave returns an already saved file', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough();
|
||||
Parse.Cloud.beforeSave(Parse.File, req => {
|
||||
@@ -3792,7 +3567,7 @@ describe('saveFile hooks', () => {
|
||||
expect(createFileSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('afterSave(Parse.File) should throw error', async () => {
|
||||
it('afterSaveFile should throw error', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
Parse.Cloud.afterSave(Parse.File, async () => {
|
||||
throw new Parse.Error(400, 'some-error-message');
|
||||
@@ -3806,7 +3581,7 @@ describe('saveFile hooks', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('afterSave(Parse.File) should call with fileObject', async done => {
|
||||
it('afterSaveFile should call with fileObject', async done => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
Parse.Cloud.beforeSave(Parse.File, async req => {
|
||||
req.file.setTags({ tagA: 'some-tag' });
|
||||
@@ -3822,7 +3597,7 @@ describe('saveFile hooks', () => {
|
||||
await file.save({ useMasterKey: true });
|
||||
});
|
||||
|
||||
it('afterSave(Parse.File) should change fileSize when file data changes', async done => {
|
||||
it('afterSaveFile should change fileSize when file data changes', async done => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
Parse.Cloud.beforeSave(Parse.File, async req => {
|
||||
expect(req.fileSize).toBe(3);
|
||||
@@ -3839,7 +3614,7 @@ describe('saveFile hooks', () => {
|
||||
await file.save({ useMasterKey: true });
|
||||
});
|
||||
|
||||
it('beforeDelete(Parse.File) should call with fileObject', async () => {
|
||||
it('beforeDeleteFile should call with fileObject', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
Parse.Cloud.beforeDelete(Parse.File, req => {
|
||||
expect(req.file).toBeInstanceOf(Parse.File);
|
||||
@@ -3851,7 +3626,7 @@ describe('saveFile hooks', () => {
|
||||
await file.destroy({ useMasterKey: true });
|
||||
});
|
||||
|
||||
it('beforeDelete(Parse.File) should throw error', async done => {
|
||||
it('beforeDeleteFile should throw error', async done => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
Parse.Cloud.beforeDelete(Parse.File, () => {
|
||||
throw new Error('some error message');
|
||||
@@ -3865,7 +3640,7 @@ describe('saveFile hooks', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('afterDelete(Parse.File) should call with fileObject', async done => {
|
||||
it('afterDeleteFile should call with fileObject', async done => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
Parse.Cloud.beforeDelete(Parse.File, req => {
|
||||
expect(req.file).toBeInstanceOf(Parse.File);
|
||||
@@ -3882,7 +3657,7 @@ describe('saveFile hooks', () => {
|
||||
await file.destroy({ useMasterKey: true });
|
||||
});
|
||||
|
||||
it('beforeSave(Parse.File) should not change file if nothing is returned', async () => {
|
||||
it('beforeSaveFile should not change file if nothing is returned', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
Parse.Cloud.beforeSave(Parse.File, () => {
|
||||
return;
|
||||
@@ -3892,7 +3667,7 @@ describe('saveFile hooks', () => {
|
||||
expect(result).toBe(file);
|
||||
});
|
||||
|
||||
it('throw custom error from beforeSave(Parse.File) ', async done => {
|
||||
it('throw custom error from beforeSaveFile', async done => {
|
||||
Parse.Cloud.beforeSave(Parse.File, () => {
|
||||
throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'It should fail');
|
||||
});
|
||||
@@ -3906,7 +3681,7 @@ describe('saveFile hooks', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('throw empty error from beforeSave(Parse.File)', async done => {
|
||||
it('throw empty error from beforeSaveFile', async done => {
|
||||
Parse.Cloud.beforeSave(Parse.File, () => {
|
||||
throw null;
|
||||
});
|
||||
@@ -3919,6 +3694,55 @@ describe('saveFile hooks', () => {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('legacy hooks', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
const logger = require('../lib/logger').logger;
|
||||
const logSpy = spyOn(logger, 'warn').and.callFake(() => {});
|
||||
const triggers = {
|
||||
beforeSaveFile(req) {
|
||||
req.file.setTags({ tagA: 'some-tag' });
|
||||
req.file.setMetadata({ foo: 'bar' });
|
||||
expect(req.triggerName).toEqual('beforeSave');
|
||||
expect(req.master).toBe(true);
|
||||
},
|
||||
afterSaveFile(req) {
|
||||
expect(req.master).toBe(true);
|
||||
expect(req.file._tags).toEqual({ tagA: 'some-tag' });
|
||||
expect(req.file._metadata).toEqual({ foo: 'bar' });
|
||||
},
|
||||
beforeDeleteFile(req) {
|
||||
expect(req.file).toBeInstanceOf(Parse.File);
|
||||
expect(req.file._name).toEqual('popeye.txt');
|
||||
expect(req.file._url).toEqual('http://www.somewhere.com/popeye.txt');
|
||||
expect(req.fileSize).toBe(null);
|
||||
},
|
||||
afterDeleteFile(req) {
|
||||
expect(req.file).toBeInstanceOf(Parse.File);
|
||||
expect(req.file._name).toEqual('popeye.txt');
|
||||
expect(req.file._url).toEqual('http://www.somewhere.com/popeye.txt');
|
||||
},
|
||||
};
|
||||
|
||||
for (const key in triggers) {
|
||||
spyOn(triggers, key).and.callThrough();
|
||||
Parse.Cloud[key](triggers[key]);
|
||||
}
|
||||
|
||||
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
|
||||
await file.save({ useMasterKey: true });
|
||||
await new Parse.File('popeye.txt', [1, 2, 3], 'text/plain').destroy({ useMasterKey: true });
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
for (const key in triggers) {
|
||||
expect(triggers[key]).toHaveBeenCalled();
|
||||
expect(logSpy).toHaveBeenCalledWith(
|
||||
`DeprecationWarning: Parse.Cloud.${key} is deprecated and will be removed in a future version. Use Parse.Cloud.${key.replace(
|
||||
'File',
|
||||
''
|
||||
)}(Parse.File, (request) => {})`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendEmail', () => {
|
||||
|
||||
@@ -15,13 +15,6 @@ describe('Cloud Code Logger', () => {
|
||||
// useful to flip to false for fine tuning :).
|
||||
silent: true,
|
||||
logLevel: undefined,
|
||||
logLevels: {
|
||||
cloudFunctionError: 'error',
|
||||
cloudFunctionSuccess: 'info',
|
||||
triggerAfter: 'info',
|
||||
triggerBeforeError: 'error',
|
||||
triggerBeforeSuccess: 'info',
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
return Parse.User.signUp('tester', 'abc')
|
||||
@@ -189,77 +182,6 @@ describe('Cloud Code Logger', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should log cloud function execution using the custom log level', async done => {
|
||||
Parse.Cloud.define('aFunction', () => {
|
||||
return 'it worked!';
|
||||
});
|
||||
|
||||
Parse.Cloud.define('bFunction', () => {
|
||||
throw new Error('Failed');
|
||||
});
|
||||
|
||||
await Parse.Cloud.run('aFunction', { foo: 'bar' }).then(() => {
|
||||
const log = spy.calls.allArgs().find(log => log[1].startsWith('Ran cloud function '))?.[0];
|
||||
expect(log).toEqual('info');
|
||||
});
|
||||
|
||||
await reconfigureServer({
|
||||
silent: true,
|
||||
logLevels: {
|
||||
cloudFunctionSuccess: 'warn',
|
||||
cloudFunctionError: 'info',
|
||||
},
|
||||
});
|
||||
|
||||
spy = spyOn(Config.get('test').loggerController.adapter, 'log').and.callThrough();
|
||||
|
||||
try {
|
||||
await Parse.Cloud.run('bFunction', { foo: 'bar' });
|
||||
throw new Error('bFunction should have failed');
|
||||
} catch {
|
||||
const log = spy.calls
|
||||
.allArgs()
|
||||
.find(log => log[1].startsWith('Failed running cloud function bFunction for '))?.[0];
|
||||
expect(log).toEqual('info');
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('should log cloud function triggers using the custom log level', async () => {
|
||||
Parse.Cloud.beforeSave('TestClass', () => {});
|
||||
Parse.Cloud.afterSave('TestClass', () => {});
|
||||
|
||||
const execTest = async (logLevel, triggerBeforeSuccess, triggerAfter) => {
|
||||
await reconfigureServer({
|
||||
silent: true,
|
||||
logLevel,
|
||||
logLevels: {
|
||||
triggerAfter,
|
||||
triggerBeforeSuccess,
|
||||
},
|
||||
});
|
||||
|
||||
spy = spyOn(Config.get('test').loggerController.adapter, 'log').and.callThrough();
|
||||
const obj = new Parse.Object('TestClass');
|
||||
await obj.save();
|
||||
|
||||
return {
|
||||
beforeSave: spy.calls
|
||||
.allArgs()
|
||||
.find(log => log[1].startsWith('beforeSave triggered for TestClass for user '))?.[0],
|
||||
afterSave: spy.calls
|
||||
.allArgs()
|
||||
.find(log => log[1].startsWith('afterSave triggered for TestClass for user '))?.[0],
|
||||
};
|
||||
};
|
||||
|
||||
let calls = await execTest('silly', 'silly', 'debug');
|
||||
expect(calls).toEqual({ beforeSave: 'silly', afterSave: 'debug' });
|
||||
|
||||
calls = await execTest('info', 'warn', 'debug');
|
||||
expect(calls).toEqual({ beforeSave: 'warn', afterSave: undefined });
|
||||
});
|
||||
|
||||
it('should log cloud function failure', done => {
|
||||
Parse.Cloud.define('aFunction', () => {
|
||||
throw 'it failed!';
|
||||
@@ -341,53 +263,4 @@ describe('Cloud Code Logger', () => {
|
||||
expect(args[0]).toBe('Parse error: ');
|
||||
expect(args[1].message).toBe('Object not found.');
|
||||
});
|
||||
|
||||
it('should log cloud function execution using the silent log level', async () => {
|
||||
await reconfigureServer({
|
||||
logLevels: {
|
||||
cloudFunctionSuccess: 'silent',
|
||||
cloudFunctionError: 'silent',
|
||||
},
|
||||
});
|
||||
Parse.Cloud.define('aFunction', () => {
|
||||
return 'it worked!';
|
||||
});
|
||||
Parse.Cloud.define('bFunction', () => {
|
||||
throw new Error('Failed');
|
||||
});
|
||||
spy = spyOn(Config.get('test').loggerController.adapter, 'log').and.callThrough();
|
||||
|
||||
await Parse.Cloud.run('aFunction', { foo: 'bar' });
|
||||
expect(spy).toHaveBeenCalledTimes(0);
|
||||
|
||||
await expectAsync(Parse.Cloud.run('bFunction', { foo: 'bar' })).toBeRejected();
|
||||
// Not "Failed running cloud function message..."
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should log cloud function triggers using the silent log level', async () => {
|
||||
await reconfigureServer({
|
||||
logLevels: {
|
||||
triggerAfter: 'silent',
|
||||
triggerBeforeSuccess: 'silent',
|
||||
triggerBeforeError: 'silent',
|
||||
},
|
||||
});
|
||||
Parse.Cloud.beforeSave('TestClassError', () => {
|
||||
throw new Error('Failed');
|
||||
});
|
||||
Parse.Cloud.beforeSave('TestClass', () => {});
|
||||
Parse.Cloud.afterSave('TestClass', () => {});
|
||||
|
||||
spy = spyOn(Config.get('test').loggerController.adapter, 'log').and.callThrough();
|
||||
|
||||
const obj = new Parse.Object('TestClass');
|
||||
await obj.save();
|
||||
expect(spy).toHaveBeenCalledTimes(0);
|
||||
|
||||
const objError = new Parse.Object('TestClassError');
|
||||
await expectAsync(objError.save()).toBeRejected();
|
||||
// Not "beforeSave failed for TestClassError for user ..."
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
const Config = require('../lib/Config');
|
||||
const DatabaseController = require('../lib/Controllers/DatabaseController.js');
|
||||
const validateQuery = DatabaseController._validateQuery;
|
||||
|
||||
@@ -362,259 +361,6 @@ describe('DatabaseController', function () {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('enableCollationCaseComparison', () => {
|
||||
const dummyStorageAdapter = {
|
||||
find: () => Promise.resolve([]),
|
||||
watch: () => Promise.resolve(),
|
||||
getAllClasses: () => Promise.resolve([]),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
Config.get(Parse.applicationId).schemaCache.clear();
|
||||
});
|
||||
|
||||
it('should force caseInsensitive to false with enableCollationCaseComparison option', async () => {
|
||||
const databaseController = new DatabaseController(dummyStorageAdapter, {
|
||||
enableCollationCaseComparison: true,
|
||||
});
|
||||
const spy = spyOn(dummyStorageAdapter, 'find');
|
||||
spy.and.callThrough();
|
||||
await databaseController.find('SomeClass', {}, { caseInsensitive: true });
|
||||
expect(spy.calls.all()[0].args[3].caseInsensitive).toEqual(false);
|
||||
});
|
||||
|
||||
it('should support caseInsensitive without enableCollationCaseComparison option', async () => {
|
||||
const databaseController = new DatabaseController(dummyStorageAdapter, {});
|
||||
const spy = spyOn(dummyStorageAdapter, 'find');
|
||||
spy.and.callThrough();
|
||||
await databaseController.find('_User', {}, { caseInsensitive: true });
|
||||
expect(spy.calls.all()[0].args[3].caseInsensitive).toEqual(true);
|
||||
});
|
||||
|
||||
it_only_db('mongo')(
|
||||
'should create insensitive indexes without enableCollationCaseComparison',
|
||||
async () => {
|
||||
await reconfigureServer({
|
||||
databaseURI: 'mongodb://localhost:27017/enableCollationCaseComparisonFalse',
|
||||
databaseAdapter: undefined,
|
||||
});
|
||||
const user = new Parse.User();
|
||||
await user.save({
|
||||
username: 'example',
|
||||
password: 'password',
|
||||
email: 'example@example.com',
|
||||
});
|
||||
const schemas = await Parse.Schema.all();
|
||||
const UserSchema = schemas.find(({ className }) => className === '_User');
|
||||
expect(UserSchema.indexes).toEqual({
|
||||
_id_: { _id: 1 },
|
||||
username_1: { username: 1 },
|
||||
case_insensitive_username: { username: 1 },
|
||||
case_insensitive_email: { email: 1 },
|
||||
email_1: { email: 1 },
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
it_only_db('mongo')(
|
||||
'should not create insensitive indexes with enableCollationCaseComparison',
|
||||
async () => {
|
||||
await reconfigureServer({
|
||||
enableCollationCaseComparison: true,
|
||||
databaseURI: 'mongodb://localhost:27017/enableCollationCaseComparisonTrue',
|
||||
databaseAdapter: undefined,
|
||||
});
|
||||
const user = new Parse.User();
|
||||
await user.save({
|
||||
username: 'example',
|
||||
password: 'password',
|
||||
email: 'example@example.com',
|
||||
});
|
||||
const schemas = await Parse.Schema.all();
|
||||
const UserSchema = schemas.find(({ className }) => className === '_User');
|
||||
expect(UserSchema.indexes).toEqual({
|
||||
_id_: { _id: 1 },
|
||||
username_1: { username: 1 },
|
||||
email_1: { email: 1 },
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('convertEmailToLowercase', () => {
|
||||
const dummyStorageAdapter = {
|
||||
createObject: () => Promise.resolve({ ops: [{}] }),
|
||||
findOneAndUpdate: () => Promise.resolve({}),
|
||||
watch: () => Promise.resolve(),
|
||||
getAllClasses: () =>
|
||||
Promise.resolve([
|
||||
{
|
||||
className: '_User',
|
||||
fields: { email: 'String' },
|
||||
indexes: {},
|
||||
classLevelPermissions: { protectedFields: {} },
|
||||
},
|
||||
]),
|
||||
};
|
||||
const dates = {
|
||||
createdAt: { iso: undefined, __type: 'Date' },
|
||||
updatedAt: { iso: undefined, __type: 'Date' },
|
||||
};
|
||||
|
||||
it('should not transform email to lower case without convertEmailToLowercase option on create', async () => {
|
||||
const databaseController = new DatabaseController(dummyStorageAdapter, {});
|
||||
const spy = spyOn(dummyStorageAdapter, 'createObject');
|
||||
spy.and.callThrough();
|
||||
await databaseController.create('_User', {
|
||||
email: 'EXAMPLE@EXAMPLE.COM',
|
||||
});
|
||||
expect(spy.calls.all()[0].args[2]).toEqual({
|
||||
email: 'EXAMPLE@EXAMPLE.COM',
|
||||
...dates,
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform email to lower case with convertEmailToLowercase option on create', async () => {
|
||||
const databaseController = new DatabaseController(dummyStorageAdapter, {
|
||||
convertEmailToLowercase: true,
|
||||
});
|
||||
const spy = spyOn(dummyStorageAdapter, 'createObject');
|
||||
spy.and.callThrough();
|
||||
await databaseController.create('_User', {
|
||||
email: 'EXAMPLE@EXAMPLE.COM',
|
||||
});
|
||||
expect(spy.calls.all()[0].args[2]).toEqual({
|
||||
email: 'example@example.com',
|
||||
...dates,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not transform email to lower case without convertEmailToLowercase option on update', async () => {
|
||||
const databaseController = new DatabaseController(dummyStorageAdapter, {});
|
||||
const spy = spyOn(dummyStorageAdapter, 'findOneAndUpdate');
|
||||
spy.and.callThrough();
|
||||
await databaseController.update('_User', { id: 'example' }, { email: 'EXAMPLE@EXAMPLE.COM' });
|
||||
expect(spy.calls.all()[0].args[3]).toEqual({
|
||||
email: 'EXAMPLE@EXAMPLE.COM',
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform email to lower case with convertEmailToLowercase option on update', async () => {
|
||||
const databaseController = new DatabaseController(dummyStorageAdapter, {
|
||||
convertEmailToLowercase: true,
|
||||
});
|
||||
const spy = spyOn(dummyStorageAdapter, 'findOneAndUpdate');
|
||||
spy.and.callThrough();
|
||||
await databaseController.update('_User', { id: 'example' }, { email: 'EXAMPLE@EXAMPLE.COM' });
|
||||
expect(spy.calls.all()[0].args[3]).toEqual({
|
||||
email: 'example@example.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not find a case insensitive user by email with convertEmailToLowercase', async () => {
|
||||
await reconfigureServer({ convertEmailToLowercase: true });
|
||||
const user = new Parse.User();
|
||||
await user.save({ username: 'EXAMPLE', email: 'EXAMPLE@EXAMPLE.COM', password: 'password' });
|
||||
|
||||
const query = new Parse.Query(Parse.User);
|
||||
query.equalTo('email', 'EXAMPLE@EXAMPLE.COM');
|
||||
const result = await query.find({ useMasterKey: true });
|
||||
expect(result.length).toEqual(0);
|
||||
|
||||
const query2 = new Parse.Query(Parse.User);
|
||||
query2.equalTo('email', 'example@example.com');
|
||||
const result2 = await query2.find({ useMasterKey: true });
|
||||
expect(result2.length).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertUsernameToLowercase', () => {
|
||||
const dummyStorageAdapter = {
|
||||
createObject: () => Promise.resolve({ ops: [{}] }),
|
||||
findOneAndUpdate: () => Promise.resolve({}),
|
||||
watch: () => Promise.resolve(),
|
||||
getAllClasses: () =>
|
||||
Promise.resolve([
|
||||
{
|
||||
className: '_User',
|
||||
fields: { username: 'String' },
|
||||
indexes: {},
|
||||
classLevelPermissions: { protectedFields: {} },
|
||||
},
|
||||
]),
|
||||
};
|
||||
const dates = {
|
||||
createdAt: { iso: undefined, __type: 'Date' },
|
||||
updatedAt: { iso: undefined, __type: 'Date' },
|
||||
};
|
||||
|
||||
it('should not transform username to lower case without convertUsernameToLowercase option on create', async () => {
|
||||
const databaseController = new DatabaseController(dummyStorageAdapter, {});
|
||||
const spy = spyOn(dummyStorageAdapter, 'createObject');
|
||||
spy.and.callThrough();
|
||||
await databaseController.create('_User', {
|
||||
username: 'EXAMPLE',
|
||||
});
|
||||
expect(spy.calls.all()[0].args[2]).toEqual({
|
||||
username: 'EXAMPLE',
|
||||
...dates,
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform username to lower case with convertUsernameToLowercase option on create', async () => {
|
||||
const databaseController = new DatabaseController(dummyStorageAdapter, {
|
||||
convertUsernameToLowercase: true,
|
||||
});
|
||||
const spy = spyOn(dummyStorageAdapter, 'createObject');
|
||||
spy.and.callThrough();
|
||||
await databaseController.create('_User', {
|
||||
username: 'EXAMPLE',
|
||||
});
|
||||
expect(spy.calls.all()[0].args[2]).toEqual({
|
||||
username: 'example',
|
||||
...dates,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not transform username to lower case without convertUsernameToLowercase option on update', async () => {
|
||||
const databaseController = new DatabaseController(dummyStorageAdapter, {});
|
||||
const spy = spyOn(dummyStorageAdapter, 'findOneAndUpdate');
|
||||
spy.and.callThrough();
|
||||
await databaseController.update('_User', { id: 'example' }, { username: 'EXAMPLE' });
|
||||
expect(spy.calls.all()[0].args[3]).toEqual({
|
||||
username: 'EXAMPLE',
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform username to lower case with convertUsernameToLowercase option on update', async () => {
|
||||
const databaseController = new DatabaseController(dummyStorageAdapter, {
|
||||
convertUsernameToLowercase: true,
|
||||
});
|
||||
const spy = spyOn(dummyStorageAdapter, 'findOneAndUpdate');
|
||||
spy.and.callThrough();
|
||||
await databaseController.update('_User', { id: 'example' }, { username: 'EXAMPLE' });
|
||||
expect(spy.calls.all()[0].args[3]).toEqual({
|
||||
username: 'example',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not find a case insensitive user by username with convertUsernameToLowercase', async () => {
|
||||
await reconfigureServer({ convertUsernameToLowercase: true });
|
||||
const user = new Parse.User();
|
||||
await user.save({ username: 'EXAMPLE', password: 'password' });
|
||||
|
||||
const query = new Parse.Query(Parse.User);
|
||||
query.equalTo('username', 'EXAMPLE');
|
||||
const result = await query.find({ useMasterKey: true });
|
||||
expect(result.length).toEqual(0);
|
||||
|
||||
const query2 = new Parse.Query(Parse.User);
|
||||
query2.equalTo('username', 'example');
|
||||
const result2 = await query2.find({ useMasterKey: true });
|
||||
expect(result2.length).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function buildCLP(pointerNames) {
|
||||
|
||||
@@ -554,7 +554,7 @@ describe('DefinedSchemas', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should not delete classes automatically', async () => {
|
||||
it('should not delete automatically classes', async () => {
|
||||
await reconfigureServer({
|
||||
schema: { definitions: [{ className: '_User' }, { className: 'Test' }] },
|
||||
});
|
||||
@@ -631,7 +631,7 @@ describe('DefinedSchemas', () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
spyOn(DefinedSchemas.prototype, 'wait').and.resolveTo();
|
||||
spyOn(logger, 'error').and.callThrough();
|
||||
spyOn(DefinedSchemas.prototype, 'createDeleteSession').and.callFake(() => {
|
||||
spyOn(Parse.Schema, 'all').and.callFake(() => {
|
||||
throw error;
|
||||
});
|
||||
|
||||
@@ -677,33 +677,4 @@ describe('DefinedSchemas', () => {
|
||||
expect(testSchema.classLevelPermissions.create).toEqual({ requiresAuthentication: true });
|
||||
expect(logger.error).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should not affect cacheAdapter', async () => {
|
||||
const server = await reconfigureServer();
|
||||
const logger = require('../lib/logger').logger;
|
||||
spyOn(logger, 'error').and.callThrough();
|
||||
const migrationOptions = {
|
||||
definitions: [
|
||||
{
|
||||
className: 'Test',
|
||||
fields: { aField: { type: 'String' } },
|
||||
indexes: { aField: { aField: 1 } },
|
||||
classLevelPermissions: {
|
||||
create: { requiresAuthentication: true },
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const cacheAdapter = {
|
||||
get: () => Promise.resolve(null),
|
||||
put: () => {},
|
||||
del: () => {},
|
||||
clear: () => {},
|
||||
connect: jasmine.createSpy('clear'),
|
||||
};
|
||||
server.config.cacheAdapter = cacheAdapter;
|
||||
await new DefinedSchemas(migrationOptions, server.config).execute();
|
||||
expect(cacheAdapter.connect).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const Auth = require('../lib/Auth');
|
||||
const Config = require('../lib/Config');
|
||||
const request = require('../lib/request');
|
||||
const MockEmailAdapterWithOptions = require('./support/MockEmailAdapterWithOptions');
|
||||
|
||||
describe('Email Verification Token Expiration: ', () => {
|
||||
it('show the invalid verification link page, if the user clicks on the verify email link after the email verify token expires', done => {
|
||||
@@ -127,7 +125,6 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
user.set('email', 'user@parse.com');
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => jasmine.timeout())
|
||||
.then(() => {
|
||||
request({
|
||||
url: sendEmailOptions.link,
|
||||
@@ -169,7 +166,6 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
user.set('email', 'user@parse.com');
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => jasmine.timeout())
|
||||
.then(() => {
|
||||
request({
|
||||
url: sendEmailOptions.link,
|
||||
@@ -217,7 +213,6 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
user.set('email', 'user@parse.com');
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => jasmine.timeout())
|
||||
.then(() => {
|
||||
request({
|
||||
url: sendEmailOptions.link,
|
||||
@@ -267,14 +262,9 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
})
|
||||
.then(() => {
|
||||
const config = Config.get('test');
|
||||
return config.database.find(
|
||||
'_User',
|
||||
{
|
||||
username: 'sets_email_verify_token_expires_at',
|
||||
},
|
||||
{},
|
||||
Auth.maintenance(config)
|
||||
);
|
||||
return config.database.find('_User', {
|
||||
username: 'sets_email_verify_token_expires_at',
|
||||
});
|
||||
})
|
||||
.then(results => {
|
||||
expect(results.length).toBe(1);
|
||||
@@ -292,223 +282,6 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('can conditionally send emails', async () => {
|
||||
let sendEmailOptions;
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: options => {
|
||||
sendEmailOptions = options;
|
||||
},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {},
|
||||
};
|
||||
const verifyUserEmails = {
|
||||
method(req) {
|
||||
expect(Object.keys(req)).toEqual(['original', 'object', 'master', 'ip', 'installationId']);
|
||||
return false;
|
||||
},
|
||||
};
|
||||
const verifySpy = spyOn(verifyUserEmails, 'method').and.callThrough();
|
||||
await reconfigureServer({
|
||||
appName: 'emailVerifyToken',
|
||||
verifyUserEmails: verifyUserEmails.method,
|
||||
emailAdapter: emailAdapter,
|
||||
emailVerifyTokenValidityDuration: 5, // 5 seconds
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
});
|
||||
const beforeSave = {
|
||||
method(req) {
|
||||
req.object.set('emailVerified', true);
|
||||
},
|
||||
};
|
||||
const saveSpy = spyOn(beforeSave, 'method').and.callThrough();
|
||||
const emailSpy = spyOn(emailAdapter, 'sendVerificationEmail').and.callThrough();
|
||||
Parse.Cloud.beforeSave(Parse.User, beforeSave.method);
|
||||
const user = new Parse.User();
|
||||
user.setUsername('sets_email_verify_token_expires_at');
|
||||
user.setPassword('expiringToken');
|
||||
user.set('email', 'user@example.com');
|
||||
await user.signUp();
|
||||
|
||||
const config = Config.get('test');
|
||||
const results = await config.database.find(
|
||||
'_User',
|
||||
{
|
||||
username: 'sets_email_verify_token_expires_at',
|
||||
},
|
||||
{},
|
||||
Auth.maintenance(config)
|
||||
);
|
||||
|
||||
expect(results.length).toBe(1);
|
||||
const user_data = results[0];
|
||||
expect(typeof user_data).toBe('object');
|
||||
expect(user_data.emailVerified).toEqual(true);
|
||||
expect(user_data._email_verify_token).toBeUndefined();
|
||||
expect(user_data._email_verify_token_expires_at).toBeUndefined();
|
||||
expect(emailSpy).not.toHaveBeenCalled();
|
||||
expect(saveSpy).toHaveBeenCalled();
|
||||
expect(sendEmailOptions).toBeUndefined();
|
||||
expect(verifySpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can conditionally send emails and allow conditional login', async () => {
|
||||
let sendEmailOptions;
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: options => {
|
||||
sendEmailOptions = options;
|
||||
},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {},
|
||||
};
|
||||
const verifyUserEmails = {
|
||||
method(req) {
|
||||
expect(Object.keys(req)).toEqual(['original', 'object', 'master', 'ip', 'installationId']);
|
||||
if (req.object.get('username') === 'no_email') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
const verifySpy = spyOn(verifyUserEmails, 'method').and.callThrough();
|
||||
await reconfigureServer({
|
||||
appName: 'emailVerifyToken',
|
||||
verifyUserEmails: verifyUserEmails.method,
|
||||
preventLoginWithUnverifiedEmail: verifyUserEmails.method,
|
||||
emailAdapter: emailAdapter,
|
||||
emailVerifyTokenValidityDuration: 5, // 5 seconds
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
});
|
||||
const user = new Parse.User();
|
||||
user.setUsername('no_email');
|
||||
user.setPassword('expiringToken');
|
||||
user.set('email', 'user@example.com');
|
||||
await user.signUp();
|
||||
expect(sendEmailOptions).toBeUndefined();
|
||||
expect(user.getSessionToken()).toBeDefined();
|
||||
expect(verifySpy).toHaveBeenCalledTimes(2);
|
||||
const user2 = new Parse.User();
|
||||
user2.setUsername('email');
|
||||
user2.setPassword('expiringToken');
|
||||
user2.set('email', 'user2@example.com');
|
||||
await user2.signUp();
|
||||
await jasmine.timeout();
|
||||
expect(user2.getSessionToken()).toBeUndefined();
|
||||
expect(sendEmailOptions).toBeDefined();
|
||||
expect(verifySpy).toHaveBeenCalledTimes(5);
|
||||
});
|
||||
|
||||
it('can conditionally send user email verification', async () => {
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: () => {},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {},
|
||||
};
|
||||
const sendVerificationEmail = {
|
||||
method(req) {
|
||||
expect(req.user).toBeDefined();
|
||||
expect(req.master).toBeDefined();
|
||||
return false;
|
||||
},
|
||||
};
|
||||
const sendSpy = spyOn(sendVerificationEmail, 'method').and.callThrough();
|
||||
await reconfigureServer({
|
||||
appName: 'emailVerifyToken',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
emailVerifyTokenValidityDuration: 5, // 5 seconds
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
sendUserEmailVerification: sendVerificationEmail.method,
|
||||
});
|
||||
const emailSpy = spyOn(emailAdapter, 'sendVerificationEmail').and.callThrough();
|
||||
const newUser = new Parse.User();
|
||||
newUser.setUsername('unsets_email_verify_token_expires_at');
|
||||
newUser.setPassword('expiringToken');
|
||||
newUser.set('email', 'user@example.com');
|
||||
await newUser.signUp();
|
||||
await Parse.User.requestEmailVerification('user@example.com');
|
||||
await jasmine.timeout();
|
||||
expect(sendSpy).toHaveBeenCalledTimes(2);
|
||||
expect(emailSpy).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('provides full user object in email verification function on email and username change', async () => {
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: () => {},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {},
|
||||
};
|
||||
const sendVerificationEmail = {
|
||||
method(req) {
|
||||
expect(req.user).toBeDefined();
|
||||
expect(req.user.id).toBeDefined();
|
||||
expect(req.user.get('createdAt')).toBeDefined();
|
||||
expect(req.user.get('updatedAt')).toBeDefined();
|
||||
expect(req.master).toBeDefined();
|
||||
return false;
|
||||
},
|
||||
};
|
||||
await reconfigureServer({
|
||||
appName: 'emailVerifyToken',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
emailVerifyTokenValidityDuration: 5,
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
sendUserEmailVerification: sendVerificationEmail.method,
|
||||
});
|
||||
const user = new Parse.User();
|
||||
user.setPassword('password');
|
||||
user.setUsername('new@example.com');
|
||||
user.setEmail('user@example.com');
|
||||
await user.save(null, { useMasterKey: true });
|
||||
|
||||
// Update email and username
|
||||
user.setUsername('new@example.com');
|
||||
user.setEmail('new@example.com');
|
||||
await user.save(null, { useMasterKey: true });
|
||||
});
|
||||
|
||||
it('beforeSave options do not change existing behaviour', async () => {
|
||||
let sendEmailOptions;
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: options => {
|
||||
sendEmailOptions = options;
|
||||
},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {},
|
||||
};
|
||||
await reconfigureServer({
|
||||
appName: 'emailVerifyToken',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
emailVerifyTokenValidityDuration: 5, // 5 seconds
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
});
|
||||
const emailSpy = spyOn(emailAdapter, 'sendVerificationEmail').and.callThrough();
|
||||
const newUser = new Parse.User();
|
||||
newUser.setUsername('unsets_email_verify_token_expires_at');
|
||||
newUser.setPassword('expiringToken');
|
||||
newUser.set('email', 'user@parse.com');
|
||||
await newUser.signUp();
|
||||
await jasmine.timeout();
|
||||
const response = await request({
|
||||
url: sendEmailOptions.link,
|
||||
followRedirects: false,
|
||||
});
|
||||
expect(response.status).toEqual(302);
|
||||
const config = Config.get('test');
|
||||
const results = await config.database.find('_User', {
|
||||
username: 'unsets_email_verify_token_expires_at',
|
||||
});
|
||||
|
||||
expect(results.length).toBe(1);
|
||||
const user = results[0];
|
||||
expect(typeof user).toBe('object');
|
||||
expect(user.emailVerified).toEqual(true);
|
||||
expect(typeof user._email_verify_token).toBe('undefined');
|
||||
expect(typeof user._email_verify_token_expires_at).toBe('undefined');
|
||||
expect(emailSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('unsets the _email_verify_token_expires_at and _email_verify_token fields in the User class if email verification is successful', done => {
|
||||
const user = new Parse.User();
|
||||
let sendEmailOptions;
|
||||
@@ -532,7 +305,6 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
user.set('email', 'user@parse.com');
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => jasmine.timeout())
|
||||
.then(() => {
|
||||
request({
|
||||
url: sendEmailOptions.link,
|
||||
@@ -592,7 +364,6 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
user.set('email', 'user@parse.com');
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => jasmine.timeout())
|
||||
.then(() => {
|
||||
return request({
|
||||
url: sendEmailOptions.link,
|
||||
@@ -728,12 +499,7 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
.then(() => {
|
||||
const config = Config.get('test');
|
||||
return config.database
|
||||
.find(
|
||||
'_User',
|
||||
{ username: 'newEmailVerifyTokenOnEmailReset' },
|
||||
{},
|
||||
Auth.maintenance(config)
|
||||
)
|
||||
.find('_User', { username: 'newEmailVerifyTokenOnEmailReset' })
|
||||
.then(results => {
|
||||
return results[0];
|
||||
});
|
||||
@@ -810,16 +576,13 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
})
|
||||
.then(response => {
|
||||
expect(response.status).toBe(200);
|
||||
})
|
||||
.then(() => jasmine.timeout())
|
||||
.then(() => {
|
||||
expect(sendVerificationEmailCallCount).toBe(2);
|
||||
expect(sendEmailOptions).toBeDefined();
|
||||
|
||||
// query for this user again
|
||||
const config = Config.get('test');
|
||||
return config.database
|
||||
.find('_User', { username: 'resends_verification_token' }, {}, Auth.maintenance(config))
|
||||
.find('_User', { username: 'resends_verification_token' })
|
||||
.then(results => {
|
||||
return results[0];
|
||||
});
|
||||
@@ -836,47 +599,11 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
done();
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
jfail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('provides function arguments in verifyUserEmails on verificationEmailRequest', async () => {
|
||||
const user = new Parse.User();
|
||||
user.setUsername('user');
|
||||
user.setPassword('pass');
|
||||
user.set('email', 'test@example.com');
|
||||
await user.signUp();
|
||||
|
||||
const verifyUserEmails = {
|
||||
method: async (params) => {
|
||||
expect(params.object).toBeInstanceOf(Parse.User);
|
||||
expect(params.ip).toBeDefined();
|
||||
expect(params.master).toBeDefined();
|
||||
expect(params.installationId).toBeDefined();
|
||||
expect(params.resendRequest).toBeTrue();
|
||||
return true;
|
||||
},
|
||||
};
|
||||
const verifyUserEmailsSpy = spyOn(verifyUserEmails, 'method').and.callThrough();
|
||||
await reconfigureServer({
|
||||
appName: 'test',
|
||||
publicServerURL: 'http://localhost:1337/1',
|
||||
verifyUserEmails: verifyUserEmails.method,
|
||||
preventLoginWithUnverifiedEmail: verifyUserEmails.method,
|
||||
preventSignupWithUnverifiedEmail: true,
|
||||
emailAdapter: MockEmailAdapterWithOptions({
|
||||
fromAddress: 'parse@example.com',
|
||||
apiKey: 'k',
|
||||
domain: 'd',
|
||||
}),
|
||||
});
|
||||
|
||||
await expectAsync(Parse.User.requestEmailVerification('test@example.com')).toBeResolved();
|
||||
expect(verifyUserEmailsSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw with invalid emailVerifyTokenReuseIfValid', async done => {
|
||||
const sendEmailOptions = [];
|
||||
const emailAdapter = {
|
||||
@@ -944,7 +671,7 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
const config = Config.get('test');
|
||||
const [userBeforeRequest] = await config.database.find('_User', {
|
||||
username: 'resends_verification_token',
|
||||
}, {}, Auth.maintenance(config));
|
||||
});
|
||||
// store this user before we make our email request
|
||||
expect(sendVerificationEmailCallCount).toBe(1);
|
||||
await new Promise(resolve => {
|
||||
@@ -964,21 +691,20 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
await jasmine.timeout();
|
||||
expect(response.status).toBe(200);
|
||||
expect(sendVerificationEmailCallCount).toBe(2);
|
||||
expect(sendEmailOptions).toBeDefined();
|
||||
|
||||
const [userAfterRequest] = await config.database.find('_User', {
|
||||
username: 'resends_verification_token',
|
||||
}, {}, Auth.maintenance(config));
|
||||
});
|
||||
|
||||
// Verify that token & expiration haven't been changed for this new request
|
||||
// verify that our token & expiration has been changed for this new request
|
||||
expect(typeof userAfterRequest).toBe('object');
|
||||
expect(userBeforeRequest._email_verify_token).toBeDefined();
|
||||
expect(userBeforeRequest._email_verify_token).toEqual(userAfterRequest._email_verify_token);
|
||||
expect(userBeforeRequest._email_verify_token_expires_at).toBeDefined();
|
||||
expect(userBeforeRequest._email_verify_token_expires_at).toEqual(userAfterRequest._email_verify_token_expires_at);
|
||||
expect(userBeforeRequest._email_verify_token_expires_at).toEqual(
|
||||
userAfterRequest._email_verify_token_expires_at
|
||||
);
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -1007,7 +733,6 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
user.set('email', 'user@parse.com');
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => jasmine.timeout())
|
||||
.then(() => {
|
||||
return request({
|
||||
url: sendEmailOptions.link,
|
||||
@@ -1246,7 +971,6 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
user.set('email', 'user@parse.com');
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => jasmine.timeout())
|
||||
.then(() => {
|
||||
request({
|
||||
url: sendEmailOptions.link,
|
||||
|
||||
@@ -55,8 +55,6 @@ describe('FilesController', () => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
expect(config.database.adapter._mongoOptions.retryWrites).toBeTrue();
|
||||
expect(config.filesController.adapter._mongoOptions.retryWrites).toBeTrue();
|
||||
expect(config.filesController.adapter._mongoOptions.enableSchemaHooks).toBeUndefined();
|
||||
expect(config.filesController.adapter._mongoOptions.schemaCacheTtl).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should create a server log on failure', done => {
|
||||
|
||||
@@ -20,22 +20,6 @@ describe_only_db('mongo')('GridFSBucket', () => {
|
||||
await db.dropDatabase();
|
||||
});
|
||||
|
||||
it('should connect to mongo with the supported database options', async () => {
|
||||
const databaseURI = 'mongodb://localhost:27017/parse';
|
||||
const gfsAdapter = new GridFSBucketAdapter(databaseURI, {
|
||||
retryWrites: true,
|
||||
// these are not supported by the mongo client
|
||||
enableSchemaHooks: true,
|
||||
schemaCacheTtl: 5000,
|
||||
maxTimeMS: 30000,
|
||||
});
|
||||
|
||||
const db = await gfsAdapter._connect();
|
||||
const status = await db.admin().serverStatus();
|
||||
expect(status.connections.current > 0).toEqual(true);
|
||||
expect(db.options?.retryWrites).toEqual(true);
|
||||
});
|
||||
|
||||
it('should save an encrypted file that can only be decrypted by a GridFS adapter with the encryptionKey', async () => {
|
||||
const unencryptedAdapter = new GridFSBucketAdapter(databaseURI);
|
||||
const encryptedAdapter = new GridFSBucketAdapter(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const httpRequest = require('../lib/request'),
|
||||
HTTPResponse = require('../lib/request').HTTPResponse,
|
||||
const httpRequest = require('../lib/cloud-code/httpRequest'),
|
||||
HTTPResponse = require('../lib/cloud-code/HTTPResponse').default,
|
||||
bodyParser = require('body-parser'),
|
||||
express = require('express');
|
||||
|
||||
|
||||
@@ -45,6 +45,10 @@ describe('Idempotency', () => {
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = process.env.PARSE_SERVER_TEST_TIMEOUT || 10000;
|
||||
});
|
||||
|
||||
// Tests
|
||||
it('should enforce idempotency for cloud code function', async () => {
|
||||
let counter = 0;
|
||||
|
||||
+40
-193
@@ -1,19 +1,11 @@
|
||||
const middlewares = require('../lib/middlewares');
|
||||
const AppCache = require('../lib/cache').AppCache;
|
||||
const { BlockList } = require('net');
|
||||
|
||||
const AppCachePut = (appId, config) =>
|
||||
AppCache.put(appId, {
|
||||
...config,
|
||||
maintenanceKeyIpsStore: new Map(),
|
||||
masterKeyIpsStore: new Map(),
|
||||
});
|
||||
|
||||
describe('middlewares', () => {
|
||||
let fakeReq, fakeRes;
|
||||
|
||||
beforeEach(() => {
|
||||
fakeReq = {
|
||||
ip: '127.0.0.1',
|
||||
originalUrl: 'http://example.com/parse/',
|
||||
url: 'http://example.com/',
|
||||
body: {
|
||||
@@ -25,7 +17,7 @@ describe('middlewares', () => {
|
||||
},
|
||||
};
|
||||
fakeRes = jasmine.createSpyObj('fakeRes', ['end', 'status']);
|
||||
AppCachePut(fakeReq.body._ApplicationId, {});
|
||||
AppCache.put(fakeReq.body._ApplicationId, {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -33,9 +25,6 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should use _ContentType if provided', done => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKeyIps: ['127.0.0.1'],
|
||||
});
|
||||
expect(fakeReq.headers['content-type']).toEqual(undefined);
|
||||
const contentType = 'image/jpeg';
|
||||
fakeReq.body._ContentType = contentType;
|
||||
@@ -47,7 +36,7 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should give invalid response when keys are configured but no key supplied', () => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
restAPIKey: 'restAPIKey',
|
||||
});
|
||||
@@ -56,7 +45,7 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should give invalid response when keys are configured but supplied key is incorrect', () => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
restAPIKey: 'restAPIKey',
|
||||
});
|
||||
@@ -66,7 +55,7 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should give invalid response when keys are configured but different key is supplied', () => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
restAPIKey: 'restAPIKey',
|
||||
});
|
||||
@@ -76,7 +65,7 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should succeed when any one of the configured keys supplied', done => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
clientKey: 'clientKey',
|
||||
masterKey: 'masterKey',
|
||||
restAPIKey: 'restAPIKey',
|
||||
@@ -89,7 +78,7 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should succeed when client key supplied but empty', done => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
clientKey: '',
|
||||
masterKey: 'masterKey',
|
||||
restAPIKey: 'restAPIKey',
|
||||
@@ -102,7 +91,7 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should succeed when no keys are configured and none supplied', done => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
});
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||
@@ -128,12 +117,10 @@ describe('middlewares', () => {
|
||||
const otherKeys = BodyKeys.filter(
|
||||
otherKey => otherKey !== infoKey && otherKey !== 'javascriptKey'
|
||||
);
|
||||
|
||||
it(`it should pull ${bodyKey} into req.info`, done => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKeyIps: ['0.0.0.0/0'],
|
||||
});
|
||||
fakeReq.ip = '127.0.0.1';
|
||||
fakeReq.body[bodyKey] = keyValue;
|
||||
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||
expect(fakeReq.body[bodyKey]).toEqual(undefined);
|
||||
expect(fakeReq.info[infoKey]).toEqual(keyValue);
|
||||
@@ -147,76 +134,41 @@ describe('middlewares', () => {
|
||||
});
|
||||
});
|
||||
|
||||
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, {
|
||||
it('should not succeed if the ip does not belong to masterKeyIps list', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['10.0.0.1'],
|
||||
masterKeyIps: ['ip1', 'ip2'],
|
||||
});
|
||||
fakeReq.ip = '127.0.0.1';
|
||||
fakeReq.ip = 'ip3';
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
|
||||
let error;
|
||||
|
||||
try {
|
||||
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toEqual(`unauthorized`);
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
`Request using master key rejected as the request IP address '127.0.0.1' is not set in Parse Server option 'masterKeyIps'.`
|
||||
);
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
expect(fakeRes.status).toHaveBeenCalledWith(403);
|
||||
});
|
||||
|
||||
it('should not succeed and log if the ip does not belong to maintenanceKeyIps list', async () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
spyOn(logger, 'error').and.callFake(() => {});
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
maintenanceKey: 'masterKey',
|
||||
maintenanceKeyIps: ['10.0.0.0', '10.0.0.1'],
|
||||
it('should succeed if the ip does belong to masterKeyIps list', done => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['ip1', 'ip2'],
|
||||
});
|
||||
fakeReq.ip = 'ip1';
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||
expect(fakeRes.status).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
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;
|
||||
}
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toEqual(`unauthorized`);
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
`Request using maintenance key rejected as the request IP address '10.0.0.2' is not set in Parse Server option 'maintenanceKeyIps'.`
|
||||
);
|
||||
});
|
||||
|
||||
it('should succeed if the ip does belong to masterKeyIps list', async () => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
it('should allow any ip to use masterKey if masterKeyIps is empty', done => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['10.0.0.1'],
|
||||
masterKeyIps: [],
|
||||
});
|
||||
fakeReq.ip = '10.0.0.1';
|
||||
fakeReq.ip = 'ip1';
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
|
||||
expect(fakeReq.auth.isMaster).toBe(true);
|
||||
});
|
||||
|
||||
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'],
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||
expect(fakeRes.status).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
fakeReq.ip = '10.0.0.1';
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
|
||||
expect(fakeReq.auth.isMaster).toBe(true);
|
||||
});
|
||||
|
||||
it('can set trust proxy', async () => {
|
||||
@@ -240,7 +192,7 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should set default Access-Control-Allow-Headers if allowHeaders are empty', () => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
allowHeaders: undefined,
|
||||
});
|
||||
const headers = {};
|
||||
@@ -253,7 +205,7 @@ describe('middlewares', () => {
|
||||
allowCrossDomain(fakeReq, res, () => {});
|
||||
expect(headers['Access-Control-Allow-Headers']).toContain(middlewares.DEFAULT_ALLOWED_HEADERS);
|
||||
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
allowHeaders: [],
|
||||
});
|
||||
allowCrossDomain(fakeReq, res, () => {});
|
||||
@@ -261,7 +213,7 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should append custom headers to Access-Control-Allow-Headers if allowHeaders provided', () => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
allowHeaders: ['Header-1', 'Header-2'],
|
||||
});
|
||||
const headers = {};
|
||||
@@ -277,7 +229,7 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should set default Access-Control-Allow-Origin if allowOrigin is empty', () => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
allowOrigin: undefined,
|
||||
});
|
||||
const headers = {};
|
||||
@@ -292,7 +244,7 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should set custom origin to Access-Control-Allow-Origin if allowOrigin is provided', () => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
allowOrigin: 'https://parseplatform.org/',
|
||||
});
|
||||
const headers = {};
|
||||
@@ -306,37 +258,8 @@ describe('middlewares', () => {
|
||||
expect(headers['Access-Control-Allow-Origin']).toEqual('https://parseplatform.org/');
|
||||
});
|
||||
|
||||
it('should support multiple origins if several are defined in allowOrigin as an array', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
allowOrigin: ['https://a.com', 'https://b.com', 'https://c.com'],
|
||||
});
|
||||
const headers = {};
|
||||
const res = {
|
||||
header: (key, value) => {
|
||||
headers[key] = value;
|
||||
},
|
||||
};
|
||||
const allowCrossDomain = middlewares.allowCrossDomain(fakeReq.body._ApplicationId);
|
||||
// Test with the first domain
|
||||
fakeReq.headers.origin = 'https://a.com';
|
||||
allowCrossDomain(fakeReq, res, () => {});
|
||||
expect(headers['Access-Control-Allow-Origin']).toEqual('https://a.com');
|
||||
// Test with the second domain
|
||||
fakeReq.headers.origin = 'https://b.com';
|
||||
allowCrossDomain(fakeReq, res, () => {});
|
||||
expect(headers['Access-Control-Allow-Origin']).toEqual('https://b.com');
|
||||
// Test with the third domain
|
||||
fakeReq.headers.origin = 'https://c.com';
|
||||
allowCrossDomain(fakeReq, res, () => {});
|
||||
expect(headers['Access-Control-Allow-Origin']).toEqual('https://c.com');
|
||||
// Test with an unauthorized domain
|
||||
fakeReq.headers.origin = 'https://unauthorized.com';
|
||||
allowCrossDomain(fakeReq, res, () => {});
|
||||
expect(headers['Access-Control-Allow-Origin']).toEqual('https://a.com');
|
||||
});
|
||||
|
||||
it('should use user provided on field userFromJWT', done => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
});
|
||||
fakeReq.userFromJWT = 'fake-user';
|
||||
@@ -347,87 +270,11 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should give invalid response when upload file without x-parse-application-id in header', () => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
});
|
||||
fakeReq.body = Buffer.from('fake-file');
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
expect(fakeRes.status).toHaveBeenCalledWith(403);
|
||||
});
|
||||
|
||||
it('should match address', () => {
|
||||
const ipv6 = '2001:0db8:85a3:0000:0000:8a2e:0370:7334';
|
||||
const anotherIpv6 = '::ffff:101.10.0.1';
|
||||
const ipv4 = '192.168.0.101';
|
||||
const localhostV6 = '::1';
|
||||
const localhostV62 = '::ffff:127.0.0.1';
|
||||
const localhostV4 = '127.0.0.1';
|
||||
|
||||
const v6 = [ipv6, anotherIpv6];
|
||||
v6.forEach(ip => {
|
||||
expect(middlewares.checkIp(ip, ['::/0'], new Map())).toBe(true);
|
||||
expect(middlewares.checkIp(ip, ['::'], new Map())).toBe(true);
|
||||
expect(middlewares.checkIp(ip, ['0.0.0.0'], new Map())).toBe(false);
|
||||
expect(middlewares.checkIp(ip, ['0.0.0.0/0'], new Map())).toBe(false);
|
||||
expect(middlewares.checkIp(ip, ['123.123.123.123'], new Map())).toBe(false);
|
||||
});
|
||||
|
||||
expect(middlewares.checkIp(ipv6, [anotherIpv6], new Map())).toBe(false);
|
||||
expect(middlewares.checkIp(ipv6, [ipv6], new Map())).toBe(true);
|
||||
expect(middlewares.checkIp(ipv6, ['2001:db8:85a3:0:0:8a2e:0:0/100'], new Map())).toBe(true);
|
||||
|
||||
expect(middlewares.checkIp(ipv4, ['::'], new Map())).toBe(false);
|
||||
expect(middlewares.checkIp(ipv4, ['::/0'], new Map())).toBe(false);
|
||||
expect(middlewares.checkIp(ipv4, ['0.0.0.0'], new Map())).toBe(true);
|
||||
expect(middlewares.checkIp(ipv4, ['0.0.0.0/0'], new Map())).toBe(true);
|
||||
expect(middlewares.checkIp(ipv4, ['123.123.123.123'], new Map())).toBe(false);
|
||||
expect(middlewares.checkIp(ipv4, [ipv4], new Map())).toBe(true);
|
||||
expect(middlewares.checkIp(ipv4, ['192.168.0.0/24'], new Map())).toBe(true);
|
||||
|
||||
expect(middlewares.checkIp(localhostV4, ['::1'], new Map())).toBe(false);
|
||||
expect(middlewares.checkIp(localhostV6, ['::1'], new Map())).toBe(true);
|
||||
// ::ffff:127.0.0.1 is a padded ipv4 address but not ::1
|
||||
expect(middlewares.checkIp(localhostV62, ['::1'], new Map())).toBe(false);
|
||||
// ::ffff:127.0.0.1 is a padded ipv4 address and is a match for 127.0.0.1
|
||||
expect(middlewares.checkIp(localhostV62, ['127.0.0.1'], new Map())).toBe(true);
|
||||
});
|
||||
|
||||
it('should match address with cache', () => {
|
||||
const ipv6 = '2001:0db8:85a3:0000:0000:8a2e:0370:7334';
|
||||
const cache1 = new Map();
|
||||
const spyBlockListCheck = spyOn(BlockList.prototype, 'check').and.callThrough();
|
||||
expect(middlewares.checkIp(ipv6, ['::'], cache1)).toBe(true);
|
||||
expect(cache1.get('2001:0db8:85a3:0000:0000:8a2e:0370:7334')).toBe(undefined);
|
||||
expect(cache1.get('allowAllIpv6')).toBe(true);
|
||||
expect(spyBlockListCheck).toHaveBeenCalledTimes(0);
|
||||
|
||||
const cache2 = new Map();
|
||||
expect(middlewares.checkIp('::1', ['::1'], cache2)).toBe(true);
|
||||
expect(cache2.get('::1')).toBe(true);
|
||||
expect(spyBlockListCheck).toHaveBeenCalledTimes(1);
|
||||
expect(middlewares.checkIp('::1', ['::1'], cache2)).toBe(true);
|
||||
expect(spyBlockListCheck).toHaveBeenCalledTimes(1);
|
||||
spyBlockListCheck.calls.reset();
|
||||
|
||||
const cache3 = new Map();
|
||||
expect(middlewares.checkIp('127.0.0.1', ['127.0.0.1'], cache3)).toBe(true);
|
||||
expect(cache3.get('127.0.0.1')).toBe(true);
|
||||
expect(spyBlockListCheck).toHaveBeenCalledTimes(1);
|
||||
expect(middlewares.checkIp('127.0.0.1', ['127.0.0.1'], cache3)).toBe(true);
|
||||
expect(spyBlockListCheck).toHaveBeenCalledTimes(1);
|
||||
spyBlockListCheck.calls.reset();
|
||||
|
||||
const cache4 = new Map();
|
||||
const ranges = ['127.0.0.1', '192.168.0.0/24'];
|
||||
// should not cache negative match
|
||||
expect(middlewares.checkIp('123.123.123.123', ranges, cache4)).toBe(false);
|
||||
expect(cache4.get('123.123.123.123')).toBe(undefined);
|
||||
expect(spyBlockListCheck).toHaveBeenCalledTimes(1);
|
||||
spyBlockListCheck.calls.reset();
|
||||
|
||||
// should not cache cidr
|
||||
expect(middlewares.checkIp('192.168.0.101', ranges, cache4)).toBe(true);
|
||||
expect(cache4.get('192.168.0.101')).toBe(undefined);
|
||||
expect(spyBlockListCheck).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ const databaseURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDataba
|
||||
const request = require('../lib/request');
|
||||
const Config = require('../lib/Config');
|
||||
const TestUtils = require('../lib/TestUtils');
|
||||
const semver = require('semver');
|
||||
|
||||
const fakeClient = {
|
||||
s: { options: { dbName: null } },
|
||||
@@ -212,89 +213,6 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('handles nested dates', async () => {
|
||||
await new Parse.Object('MyClass', {
|
||||
foo: {
|
||||
test: {
|
||||
date: new Date(),
|
||||
},
|
||||
},
|
||||
bar: {
|
||||
date: new Date(),
|
||||
},
|
||||
date: new Date(),
|
||||
}).save();
|
||||
const adapter = Config.get(Parse.applicationId).database.adapter;
|
||||
const [object] = await adapter._rawFind('MyClass', {});
|
||||
expect(object.date instanceof Date).toBeTrue();
|
||||
expect(object.bar.date instanceof Date).toBeTrue();
|
||||
expect(object.foo.test.date instanceof Date).toBeTrue();
|
||||
});
|
||||
|
||||
it('handles nested dates in array ', async () => {
|
||||
await new Parse.Object('MyClass', {
|
||||
foo: {
|
||||
test: {
|
||||
date: [new Date()],
|
||||
},
|
||||
},
|
||||
bar: {
|
||||
date: [new Date()],
|
||||
},
|
||||
date: [new Date()],
|
||||
}).save();
|
||||
const adapter = Config.get(Parse.applicationId).database.adapter;
|
||||
const [object] = await adapter._rawFind('MyClass', {});
|
||||
expect(object.date[0] instanceof Date).toBeTrue();
|
||||
expect(object.bar.date[0] instanceof Date).toBeTrue();
|
||||
expect(object.foo.test.date[0] instanceof Date).toBeTrue();
|
||||
const obj = await new Parse.Query('MyClass').first({ useMasterKey: true });
|
||||
expect(obj.get('date')[0] instanceof Date).toBeTrue();
|
||||
expect(obj.get('bar').date[0] instanceof Date).toBeTrue();
|
||||
expect(obj.get('foo').test.date[0] instanceof Date).toBeTrue();
|
||||
});
|
||||
|
||||
it('upserts with $setOnInsert', async () => {
|
||||
const uuid = require('uuid');
|
||||
const uuid1 = uuid.v4();
|
||||
const uuid2 = uuid.v4();
|
||||
const schema = {
|
||||
className: 'MyClass',
|
||||
fields: {
|
||||
x: { type: 'Number' },
|
||||
count: { type: 'Number' },
|
||||
},
|
||||
classLevelPermissions: {},
|
||||
};
|
||||
|
||||
const myClassSchema = new Parse.Schema(schema.className);
|
||||
myClassSchema.setCLP(schema.classLevelPermissions);
|
||||
await myClassSchema.save();
|
||||
|
||||
const query = {
|
||||
x: 1,
|
||||
};
|
||||
const update = {
|
||||
objectId: {
|
||||
__op: 'SetOnInsert',
|
||||
amount: uuid1,
|
||||
},
|
||||
count: {
|
||||
__op: 'Increment',
|
||||
amount: 1,
|
||||
},
|
||||
};
|
||||
await Parse.Server.database.update('MyClass', query, update, { upsert: true });
|
||||
update.objectId.amount = uuid2;
|
||||
await Parse.Server.database.update('MyClass', query, update, { upsert: true });
|
||||
|
||||
const res = await Parse.Server.database.find(schema.className, {}, {});
|
||||
expect(res.length).toBe(1);
|
||||
expect(res[0].objectId).toBe(uuid1);
|
||||
expect(res[0].count).toBe(2);
|
||||
expect(res[0].x).toBe(1);
|
||||
});
|
||||
|
||||
it('handles updating a single object with array, object date', done => {
|
||||
const adapter = new MongoStorageAdapter({ uri: databaseURI });
|
||||
|
||||
@@ -483,7 +401,11 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
expect(schemaAfterDeletion.fields.test).toBeUndefined();
|
||||
});
|
||||
|
||||
if (process.env.MONGODB_TOPOLOGY === 'replicaset') {
|
||||
if (
|
||||
semver.satisfies(process.env.MONGODB_VERSION, '>=4.0.4') &&
|
||||
process.env.MONGODB_TOPOLOGY === 'replicaset' &&
|
||||
process.env.MONGODB_STORAGE_ENGINE === 'wiredTiger'
|
||||
) {
|
||||
describe('transactions', () => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
+2
-2
@@ -87,7 +87,7 @@ describe('OAuth', function () {
|
||||
done();
|
||||
}
|
||||
|
||||
xit('GET request for a resource that requires OAuth should fail with invalid credentials', done => {
|
||||
it('GET request for a resource that requires OAuth should fail with invalid credentials', done => {
|
||||
/*
|
||||
This endpoint has been chosen to make a request to an endpoint that requires OAuth which fails due to missing authentication.
|
||||
Any other endpoint from the Twitter API that requires OAuth can be used instead in case the currently used endpoint deprecates.
|
||||
@@ -105,7 +105,7 @@ describe('OAuth', function () {
|
||||
});
|
||||
});
|
||||
|
||||
xit('POST request for a resource that requires OAuth should fail with invalid credentials', done => {
|
||||
it('POST request for a resource that requires OAuth should fail with invalid credentials', done => {
|
||||
/*
|
||||
This endpoint has been chosen to make a request to an endpoint that requires OAuth which fails due to missing authentication.
|
||||
Any other endpoint from the Twitter API that requires OAuth can be used instead in case the currently used endpoint deprecates.
|
||||
|
||||
@@ -749,7 +749,6 @@ describe('Pages Router', () => {
|
||||
user.setPassword('examplePassword');
|
||||
user.set('email', 'mail@example.com');
|
||||
await user.signUp();
|
||||
await jasmine.timeout();
|
||||
|
||||
const link = sendVerificationEmail.calls.all()[0].args[0].link;
|
||||
const linkWithLocale = new URL(link);
|
||||
@@ -778,7 +777,6 @@ describe('Pages Router', () => {
|
||||
user.setPassword('examplePassword');
|
||||
user.set('email', 'mail@example.com');
|
||||
await user.signUp();
|
||||
await jasmine.timeout();
|
||||
|
||||
const link = sendVerificationEmail.calls.all()[0].args[0].link;
|
||||
const linkWithLocale = new URL(link);
|
||||
@@ -832,7 +830,6 @@ describe('Pages Router', () => {
|
||||
user.setPassword('examplePassword');
|
||||
user.set('email', 'mail@example.com');
|
||||
await user.signUp();
|
||||
await jasmine.timeout();
|
||||
|
||||
const link = sendVerificationEmail.calls.all()[0].args[0].link;
|
||||
const linkWithLocale = new URL(link);
|
||||
@@ -849,8 +846,6 @@ describe('Pages Router', () => {
|
||||
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);
|
||||
@@ -1195,7 +1190,6 @@ describe('Pages Router', () => {
|
||||
user.setPassword('examplePassword');
|
||||
user.set('email', 'mail@example.com');
|
||||
await user.signUp();
|
||||
await jasmine.timeout();
|
||||
|
||||
const link = sendVerificationEmail.calls.all()[0].args[0].link;
|
||||
const linkResponse = await request({
|
||||
@@ -1203,6 +1197,7 @@ describe('Pages Router', () => {
|
||||
followRedirects: false,
|
||||
});
|
||||
expect(linkResponse.status).toBe(200);
|
||||
|
||||
const pagePath = pageResponse.calls.all()[0].args[0];
|
||||
expect(pagePath).toMatch(new RegExp(`\/${pages.emailVerificationSuccess.defaultFile}`));
|
||||
});
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
const request = require('../lib/request');
|
||||
|
||||
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
const pushCompleted = async pushId => {
|
||||
const query = new Parse.Query('_PushStatus');
|
||||
query.equalTo('objectId', pushId);
|
||||
let result = await query.first({ useMasterKey: true });
|
||||
while (!(result && result.get('status') === 'succeeded')) {
|
||||
await jasmine.timeout();
|
||||
await sleep(100);
|
||||
result = await query.first({ useMasterKey: true });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1267,26 +1267,6 @@ describe('miscellaneous', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('test cloud function query parameters with array of pointers', async () => {
|
||||
await reconfigureServer({ encodeParseObjectInCloudFunction: false });
|
||||
Parse.Cloud.define('echoParams', req => {
|
||||
return req.params;
|
||||
});
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Javascript-Key': 'test',
|
||||
};
|
||||
const response = await request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/functions/echoParams',
|
||||
body: '{"arr": [{ "__type": "Pointer", "className": "PointerTest" }]}',
|
||||
});
|
||||
const res = response.data.result;
|
||||
expect(res.arr.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('can handle null params in cloud functions (regression test for #1742)', done => {
|
||||
Parse.Cloud.define('func', request => {
|
||||
expect(request.params.nullParam).toEqual(null);
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
const Config = require('../lib/Config');
|
||||
|
||||
describe('Config Keys', () => {
|
||||
const invalidKeyErrorMessage = 'Invalid key\\(s\\) found in Parse Server configuration';
|
||||
let loggerErrorSpy;
|
||||
|
||||
beforeEach(async () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
loggerErrorSpy = spyOn(logger, 'error').and.callThrough();
|
||||
spyOn(Config, 'validateOptions').and.callFake(() => {});
|
||||
});
|
||||
|
||||
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], '');
|
||||
expect(error).toMatch(invalidKeyErrorMessage);
|
||||
});
|
||||
|
||||
it('recognizes invalid keys in pages.customUrls', async () => {
|
||||
await expectAsync(reconfigureServer({
|
||||
...defaultConfiguration,
|
||||
pages: {
|
||||
customUrls: {
|
||||
invalidKey: 1,
|
||||
EmailVerificationSendFail: 1,
|
||||
}
|
||||
}
|
||||
})).toBeResolved();
|
||||
const error = loggerErrorSpy.calls.all().reduce((s, call) => s += call.args[0], '');
|
||||
expect(error).toMatch(invalidKeyErrorMessage);
|
||||
expect(error).toMatch(`invalidKey`);
|
||||
expect(error).toMatch(`EmailVerificationSendFail`);
|
||||
});
|
||||
|
||||
it('recognizes invalid keys in liveQueryServerOptions', async () => {
|
||||
await expectAsync(reconfigureServer({
|
||||
...defaultConfiguration,
|
||||
liveQueryServerOptions: {
|
||||
invalidKey: 1,
|
||||
MasterKey: 1,
|
||||
}
|
||||
})).toBeResolved();
|
||||
const error = loggerErrorSpy.calls.all().reduce((s, call) => s += call.args[0], '');
|
||||
expect(error).toMatch(invalidKeyErrorMessage);
|
||||
expect(error).toMatch(`MasterKey`);
|
||||
});
|
||||
|
||||
it('recognizes invalid keys in rateLimit', async () => {
|
||||
await expectAsync(reconfigureServer({
|
||||
...defaultConfiguration,
|
||||
rateLimit: [
|
||||
{ invalidKey: 1 },
|
||||
{ RequestPath: 1 },
|
||||
{ RequestTimeWindow: 1 },
|
||||
]
|
||||
})).toBeRejected();
|
||||
const error = loggerErrorSpy.calls.all().reduce((s, call) => s += call.args[0], '');
|
||||
expect(error).toMatch(invalidKeyErrorMessage);
|
||||
expect(error).toMatch('rateLimit\\[0\\]\\.invalidKey');
|
||||
expect(error).toMatch('rateLimit\\[1\\]\\.RequestPath');
|
||||
expect(error).toMatch('rateLimit\\[2\\]\\.RequestTimeWindow');
|
||||
});
|
||||
|
||||
it('recognizes valid keys in default configuration', async () => {
|
||||
await expectAsync(reconfigureServer({
|
||||
...defaultConfiguration,
|
||||
})).toBeResolved();
|
||||
expect(loggerErrorSpy.calls.all().reduce((s, call) => s += call.args[0], '')).not.toMatch(invalidKeyErrorMessage);
|
||||
});
|
||||
|
||||
it_only_db('mongo')('recognizes valid keys in databaseOptions (MongoDB)', async () => {
|
||||
await expectAsync(reconfigureServer({
|
||||
databaseURI: 'mongodb://localhost:27017/parse',
|
||||
filesAdapter: null,
|
||||
databaseAdapter: null,
|
||||
databaseOptions: {
|
||||
retryWrites: true,
|
||||
maxTimeMS: 1000,
|
||||
maxStalenessSeconds: 10,
|
||||
maxPoolSize: 10,
|
||||
},
|
||||
})).toBeResolved();
|
||||
expect(loggerErrorSpy.calls.all().reduce((s, call) => s += call.args[0], '')).not.toMatch(invalidKeyErrorMessage);
|
||||
});
|
||||
});
|
||||
+73
-293
@@ -37,14 +37,8 @@ describe('Parse.File testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('works with _ContentType', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
fileExtensions: ['*'],
|
||||
},
|
||||
});
|
||||
let response = await request({
|
||||
it('works with _ContentType', done => {
|
||||
request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/files/file',
|
||||
body: JSON.stringify({
|
||||
@@ -53,18 +47,21 @@ describe('Parse.File testing', () => {
|
||||
_ContentType: 'text/html',
|
||||
base64: 'PGh0bWw+PC9odG1sPgo=',
|
||||
}),
|
||||
}).then(response => {
|
||||
const b = response.data;
|
||||
expect(b.name).toMatch(/_file.html/);
|
||||
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.html$/);
|
||||
request({ url: b.url }).then(response => {
|
||||
const body = response.text;
|
||||
try {
|
||||
expect(response.headers['content-type']).toMatch('^text/html');
|
||||
expect(body).toEqual('<html></html>\n');
|
||||
} catch (e) {
|
||||
jfail(e);
|
||||
}
|
||||
done();
|
||||
});
|
||||
});
|
||||
const b = response.data;
|
||||
expect(b.name).toMatch(/_file.html/);
|
||||
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.html$/);
|
||||
response = await request({ url: b.url });
|
||||
const body = response.text;
|
||||
try {
|
||||
expect(response.headers['content-type']).toMatch('^text/html');
|
||||
expect(body).toEqual('<html></html>\n');
|
||||
} catch (e) {
|
||||
jfail(e);
|
||||
}
|
||||
});
|
||||
|
||||
it('works without Content-Type', done => {
|
||||
@@ -354,28 +351,25 @@ describe('Parse.File testing', () => {
|
||||
ok(object.toJSON().file.url);
|
||||
});
|
||||
|
||||
it('content-type used with no extension', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
fileExtensions: ['*'],
|
||||
},
|
||||
});
|
||||
it('content-type used with no extension', done => {
|
||||
const headers = {
|
||||
'Content-Type': 'text/html',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
let response = await request({
|
||||
request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/files/file',
|
||||
body: 'fee fi fo',
|
||||
}).then(response => {
|
||||
const b = response.data;
|
||||
expect(b.name).toMatch(/\.html$/);
|
||||
request({ url: b.url }).then(response => {
|
||||
expect(response.headers['content-type']).toMatch(/^text\/html/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
const b = response.data;
|
||||
expect(b.name).toMatch(/\.html$/);
|
||||
response = await request({ url: b.url });
|
||||
expect(response.headers['content-type']).toMatch(/^text\/html/);
|
||||
});
|
||||
|
||||
it('filename is url encoded', done => {
|
||||
@@ -789,11 +783,59 @@ describe('Parse.File testing', () => {
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
Range: 'bytes=abc-efs',
|
||||
},
|
||||
}).catch(e => e);
|
||||
expect(file.headers['content-range']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('supports bytes range if start and end undefined', async () => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
const response = await request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1//files/file.txt ',
|
||||
body: repeat('argle bargle', 100),
|
||||
});
|
||||
const b = response.data;
|
||||
const file = await request({
|
||||
url: b.url,
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
},
|
||||
}).catch(e => e);
|
||||
expect(file.headers['content-range']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('supports bytes range if end is greater than size', async () => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
const response = await request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1//files/file.txt ',
|
||||
body: repeat('argle bargle', 100),
|
||||
});
|
||||
const b = response.data;
|
||||
const file = await request({
|
||||
url: b.url,
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
Range: 'bytes=0-2000',
|
||||
},
|
||||
}).catch(e => e);
|
||||
expect(file.headers['content-range']).toBe('bytes 0-1212/1212');
|
||||
});
|
||||
|
||||
it('supports bytes range if end is greater than size', async () => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
@@ -1256,268 +1298,6 @@ describe('Parse.File testing', () => {
|
||||
await expectAsync(reconfigureServer({ fileUpload: { [key]: value } })).toBeResolved();
|
||||
}
|
||||
}
|
||||
await expectAsync(
|
||||
reconfigureServer({
|
||||
fileUpload: {
|
||||
fileExtensions: 1,
|
||||
},
|
||||
})
|
||||
).toBeRejectedWith('fileUpload.fileExtensions must be an array.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fileExtensions', () => {
|
||||
it('works with _ContentType', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
fileExtensions: ['png'],
|
||||
},
|
||||
});
|
||||
await expectAsync(
|
||||
request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/files/file',
|
||||
body: JSON.stringify({
|
||||
_ApplicationId: 'test',
|
||||
_JavaScriptKey: 'test',
|
||||
_ContentType: 'text/html',
|
||||
base64: 'PGh0bWw+PC9odG1sPgo=',
|
||||
}),
|
||||
}).catch(e => {
|
||||
throw new Error(e.data.error);
|
||||
})
|
||||
).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`)
|
||||
);
|
||||
});
|
||||
|
||||
it('works without Content-Type', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
},
|
||||
});
|
||||
const headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
await expectAsync(
|
||||
request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/files/file.html',
|
||||
body: '<html></html>\n',
|
||||
}).catch(e => {
|
||||
throw new Error(e.data.error);
|
||||
})
|
||||
).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`)
|
||||
);
|
||||
});
|
||||
|
||||
it('default should allow common types', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
},
|
||||
});
|
||||
for (const type of ['plain', 'txt', 'png', 'jpg', 'gif', 'doc']) {
|
||||
const file = new Parse.File(`parse-server-logo.${type}`, { base64: 'ParseA==' });
|
||||
await file.save();
|
||||
}
|
||||
});
|
||||
|
||||
it('works with a period in the file name', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
fileExtensions: ['^[^hH][^tT][^mM][^lL]?$'],
|
||||
},
|
||||
});
|
||||
const headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
|
||||
const values = ['file.png.html', 'file.txt.png.html', 'file.png.txt.html'];
|
||||
|
||||
for (const value of values) {
|
||||
await expectAsync(
|
||||
request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: `http://localhost:8378/1/files/${value}`,
|
||||
body: '<html></html>\n',
|
||||
}).catch(e => {
|
||||
throw new Error(e.data.error);
|
||||
})
|
||||
).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('works to stop invalid filenames', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
fileExtensions: ['^[^hH][^tT][^mM][^lL]?$'],
|
||||
},
|
||||
});
|
||||
const headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
|
||||
const values = [
|
||||
'!invalid.png',
|
||||
'.png',
|
||||
'.html',
|
||||
' .html',
|
||||
'.png.html',
|
||||
'~invalid.png',
|
||||
'-invalid.png',
|
||||
];
|
||||
|
||||
for (const value of values) {
|
||||
await expectAsync(
|
||||
request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: `http://localhost:8378/1/files/${value}`,
|
||||
body: '<html></html>\n',
|
||||
}).catch(e => {
|
||||
throw new Error(e.data.error);
|
||||
})
|
||||
).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.INVALID_FILE_NAME, `Filename contains invalid characters.`)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('allows file without extension', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
fileExtensions: ['^[^hH][^tT][^mM][^lL]?$'],
|
||||
},
|
||||
});
|
||||
const headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
|
||||
const values = ['filenamewithoutextension'];
|
||||
|
||||
for (const value of values) {
|
||||
await expectAsync(
|
||||
request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: `http://localhost:8378/1/files/${value}`,
|
||||
body: '<html></html>\n',
|
||||
}).catch(e => {
|
||||
throw new Error(e.data.error);
|
||||
})
|
||||
).toBeResolved();
|
||||
}
|
||||
});
|
||||
|
||||
it('works with array', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
fileExtensions: ['jpg', 'wav'],
|
||||
},
|
||||
});
|
||||
await expectAsync(
|
||||
request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/files/file',
|
||||
body: JSON.stringify({
|
||||
_ApplicationId: 'test',
|
||||
_JavaScriptKey: 'test',
|
||||
_ContentType: 'text/html',
|
||||
base64: 'PGh0bWw+PC9odG1sPgo=',
|
||||
}),
|
||||
}).catch(e => {
|
||||
throw new Error(e.data.error);
|
||||
})
|
||||
).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`)
|
||||
);
|
||||
await expectAsync(
|
||||
request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/files/file',
|
||||
body: JSON.stringify({
|
||||
_ApplicationId: 'test',
|
||||
_JavaScriptKey: 'test',
|
||||
_ContentType: 'image/jpg',
|
||||
base64: 'PGh0bWw+PC9odG1sPgo=',
|
||||
}),
|
||||
})
|
||||
).toBeResolved();
|
||||
await expectAsync(
|
||||
request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/files/file',
|
||||
body: JSON.stringify({
|
||||
_ApplicationId: 'test',
|
||||
_JavaScriptKey: 'test',
|
||||
_ContentType: 'audio/wav',
|
||||
base64: 'UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA',
|
||||
}),
|
||||
})
|
||||
).toBeResolved();
|
||||
});
|
||||
|
||||
it('works with array without Content-Type', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
fileExtensions: ['jpg'],
|
||||
},
|
||||
});
|
||||
const headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
await expectAsync(
|
||||
request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/files/file.html',
|
||||
body: '<html></html>\n',
|
||||
}).catch(e => {
|
||||
throw new Error(e.data.error);
|
||||
})
|
||||
).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`)
|
||||
);
|
||||
});
|
||||
|
||||
it('works with array with correct file type', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
fileExtensions: ['html'],
|
||||
},
|
||||
});
|
||||
const response = await request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/files/file',
|
||||
body: JSON.stringify({
|
||||
_ApplicationId: 'test',
|
||||
_JavaScriptKey: 'test',
|
||||
_ContentType: 'text/html',
|
||||
base64: 'PGh0bWw+PC9odG1sPgo=',
|
||||
}),
|
||||
});
|
||||
const b = response.data;
|
||||
expect(b.name).toMatch(/_file.html$/);
|
||||
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.html$/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,7 +47,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
obj.set('location', point);
|
||||
obj.set('name', 'Zhoul');
|
||||
await obj.save();
|
||||
request({
|
||||
Parse.Cloud.httpRequest({
|
||||
url: 'http://localhost:8378/1/classes/TestObject/' + obj.id,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
|
||||
+20
-420
@@ -8,7 +8,7 @@ require('./helper');
|
||||
const { updateCLP } = require('./support/dev');
|
||||
|
||||
const pluralize = require('pluralize');
|
||||
const { getMainDefinition } = require('@apollo/client/utilities');
|
||||
const { getMainDefinition } = require('apollo-utilities');
|
||||
const { createUploadLink } = require('apollo-upload-client');
|
||||
const { SubscriptionClient } = require('subscriptions-transport-ws');
|
||||
const { WebSocketLink } = require('@apollo/client/link/ws');
|
||||
@@ -49,9 +49,7 @@ describe('ParseGraphQLServer', () => {
|
||||
let parseGraphQLServer;
|
||||
|
||||
beforeEach(async () => {
|
||||
parseServer = await global.reconfigureServer({
|
||||
maxUploadSize: '1kb',
|
||||
});
|
||||
parseServer = await global.reconfigureServer({});
|
||||
parseGraphQLServer = new ParseGraphQLServer(parseServer, {
|
||||
graphQLPath: '/graphql',
|
||||
playgroundPath: '/playground',
|
||||
@@ -124,16 +122,15 @@ describe('ParseGraphQLServer', () => {
|
||||
info: new Object(),
|
||||
config: new Object(),
|
||||
auth: new Object(),
|
||||
get: () => {},
|
||||
};
|
||||
const res = {
|
||||
set: () => {},
|
||||
};
|
||||
|
||||
it("should return schema and context with req's info, config and auth", async () => {
|
||||
const options = await parseGraphQLServer._getGraphQLOptions();
|
||||
expect(options.multipart).toEqual({
|
||||
fileSize: 20971520,
|
||||
});
|
||||
expect(options.schema).toEqual(parseGraphQLServer.parseGraphQLSchema.graphQLSchema);
|
||||
const contextResponse = await options.context({ req, res });
|
||||
const contextResponse = options.context({ req });
|
||||
expect(contextResponse.info).toEqual(req.info);
|
||||
expect(contextResponse.config).toEqual(req.config);
|
||||
expect(contextResponse.auth).toEqual(req.auth);
|
||||
@@ -295,37 +292,30 @@ describe('ParseGraphQLServer', () => {
|
||||
let objects = [];
|
||||
|
||||
async function prepareData() {
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
user1 = new Parse.User();
|
||||
user1.setUsername('user1');
|
||||
user1.setPassword('user1');
|
||||
user1.setEmail('user1@user1.user1');
|
||||
user1.setACL(acl);
|
||||
await user1.signUp();
|
||||
|
||||
user2 = new Parse.User();
|
||||
user2.setUsername('user2');
|
||||
user2.setPassword('user2');
|
||||
user2.setACL(acl);
|
||||
await user2.signUp();
|
||||
|
||||
user3 = new Parse.User();
|
||||
user3.setUsername('user3');
|
||||
user3.setPassword('user3');
|
||||
user3.setACL(acl);
|
||||
await user3.signUp();
|
||||
|
||||
user4 = new Parse.User();
|
||||
user4.setUsername('user4');
|
||||
user4.setPassword('user4');
|
||||
user4.setACL(acl);
|
||||
await user4.signUp();
|
||||
|
||||
user5 = new Parse.User();
|
||||
user5.setUsername('user5');
|
||||
user5.setPassword('user5');
|
||||
user5.setACL(acl);
|
||||
await user5.signUp();
|
||||
|
||||
const roleACL = new Parse.ACL();
|
||||
@@ -435,7 +425,7 @@ describe('ParseGraphQLServer', () => {
|
||||
const expressApp = express();
|
||||
httpServer = http.createServer(expressApp);
|
||||
expressApp.use('/parse', parseServer.app);
|
||||
parseLiveQueryServer = await ParseServer.createLiveQueryServer(httpServer, {
|
||||
parseLiveQueryServer = ParseServer.createLiveQueryServer(httpServer, {
|
||||
port: 1338,
|
||||
});
|
||||
parseGraphQLServer.applyGraphQL(expressApp);
|
||||
@@ -952,7 +942,8 @@ describe('ParseGraphQLServer', () => {
|
||||
).data['__type'].inputFields
|
||||
.map(field => field.name)
|
||||
.sort();
|
||||
expect(inputFields).toEqual(['authData', 'clientMutationId', 'password', 'username']);
|
||||
|
||||
expect(inputFields).toEqual(['clientMutationId', 'password', 'username']);
|
||||
});
|
||||
|
||||
it('should have clientMutationId in log in mutation payload', async () => {
|
||||
@@ -5278,6 +5269,7 @@ describe('ParseGraphQLServer', () => {
|
||||
|
||||
it('should only count', async () => {
|
||||
await prepareData();
|
||||
|
||||
await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
|
||||
|
||||
const where = {
|
||||
@@ -6835,7 +6827,7 @@ describe('ParseGraphQLServer', () => {
|
||||
|
||||
describe('Files Mutations', () => {
|
||||
describe('Create', () => {
|
||||
it('should return File object', async () => {
|
||||
it_only_node_version('<17')('should return File object', async () => {
|
||||
const clientMutationId = uuidv4();
|
||||
|
||||
parseServer = await global.reconfigureServer({
|
||||
@@ -7035,66 +7027,7 @@ describe('ParseGraphQLServer', () => {
|
||||
});
|
||||
|
||||
describe('Users Mutations', () => {
|
||||
const challengeAdapter = {
|
||||
validateAuthData: () => Promise.resolve({ response: { someData: true } }),
|
||||
validateAppId: () => Promise.resolve(),
|
||||
challenge: () => Promise.resolve({ someData: true }),
|
||||
options: { anOption: true },
|
||||
};
|
||||
|
||||
it('should create user and return authData response', async () => {
|
||||
parseServer = await global.reconfigureServer({
|
||||
publicServerURL: 'http://localhost:13377/parse',
|
||||
auth: {
|
||||
challengeAdapter,
|
||||
},
|
||||
});
|
||||
const clientMutationId = uuidv4();
|
||||
|
||||
const result = await apolloClient.mutate({
|
||||
mutation: gql`
|
||||
mutation createUser($input: CreateUserInput!) {
|
||||
createUser(input: $input) {
|
||||
clientMutationId
|
||||
user {
|
||||
id
|
||||
authDataResponse
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
clientMutationId,
|
||||
fields: {
|
||||
authData: {
|
||||
challengeAdapter: {
|
||||
id: 'challengeAdapter',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
context: {
|
||||
headers: {
|
||||
'X-Parse-Master-Key': 'test',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.data.createUser.clientMutationId).toEqual(clientMutationId);
|
||||
expect(result.data.createUser.user.authDataResponse).toEqual({
|
||||
challengeAdapter: { someData: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('should sign user up', async () => {
|
||||
parseServer = await global.reconfigureServer({
|
||||
publicServerURL: 'http://localhost:13377/parse',
|
||||
auth: {
|
||||
challengeAdapter,
|
||||
},
|
||||
});
|
||||
const clientMutationId = uuidv4();
|
||||
const userSchema = new Parse.Schema('_User');
|
||||
userSchema.addString('someField');
|
||||
@@ -7111,7 +7044,6 @@ describe('ParseGraphQLServer', () => {
|
||||
sessionToken
|
||||
user {
|
||||
someField
|
||||
authDataResponse
|
||||
aPointer {
|
||||
id
|
||||
username
|
||||
@@ -7127,17 +7059,11 @@ describe('ParseGraphQLServer', () => {
|
||||
fields: {
|
||||
username: 'user1',
|
||||
password: 'user1',
|
||||
authData: {
|
||||
challengeAdapter: {
|
||||
id: 'challengeAdapter',
|
||||
},
|
||||
},
|
||||
aPointer: {
|
||||
createAndLink: {
|
||||
username: 'user2',
|
||||
password: 'user2',
|
||||
someField: 'someValue2',
|
||||
ACL: { public: { read: true, write: true } },
|
||||
},
|
||||
},
|
||||
someField: 'someValue',
|
||||
@@ -7152,9 +7078,6 @@ describe('ParseGraphQLServer', () => {
|
||||
expect(result.data.signUp.viewer.user.aPointer.id).toBeDefined();
|
||||
expect(result.data.signUp.viewer.user.aPointer.username).toEqual('user2');
|
||||
expect(typeof result.data.signUp.viewer.sessionToken).toBe('string');
|
||||
expect(result.data.signUp.viewer.user.authDataResponse).toEqual({
|
||||
challengeAdapter: { someData: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('should login with user', async () => {
|
||||
@@ -7163,7 +7086,6 @@ describe('ParseGraphQLServer', () => {
|
||||
parseServer = await global.reconfigureServer({
|
||||
publicServerURL: 'http://localhost:13377/parse',
|
||||
auth: {
|
||||
challengeAdapter,
|
||||
myAuth: {
|
||||
module: global.mockCustomAuthenticator('parse', 'graphql'),
|
||||
},
|
||||
@@ -7183,7 +7105,6 @@ describe('ParseGraphQLServer', () => {
|
||||
sessionToken
|
||||
user {
|
||||
someField
|
||||
authDataResponse
|
||||
aPointer {
|
||||
id
|
||||
username
|
||||
@@ -7197,7 +7118,6 @@ describe('ParseGraphQLServer', () => {
|
||||
input: {
|
||||
clientMutationId,
|
||||
authData: {
|
||||
challengeAdapter: { id: 'challengeAdapter' },
|
||||
myAuth: {
|
||||
id: 'parse',
|
||||
password: 'graphql',
|
||||
@@ -7210,7 +7130,6 @@ describe('ParseGraphQLServer', () => {
|
||||
username: 'user2',
|
||||
password: 'user2',
|
||||
someField: 'someValue2',
|
||||
ACL: { public: { read: true, write: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -7224,92 +7143,9 @@ describe('ParseGraphQLServer', () => {
|
||||
expect(typeof result.data.logInWith.viewer.sessionToken).toBe('string');
|
||||
expect(result.data.logInWith.viewer.user.aPointer.id).toBeDefined();
|
||||
expect(result.data.logInWith.viewer.user.aPointer.username).toEqual('user2');
|
||||
expect(result.data.logInWith.viewer.user.authDataResponse).toEqual({
|
||||
challengeAdapter: { someData: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle challenge', async () => {
|
||||
const clientMutationId = uuidv4();
|
||||
|
||||
spyOn(challengeAdapter, 'challenge').and.callThrough();
|
||||
parseServer = await global.reconfigureServer({
|
||||
publicServerURL: 'http://localhost:13377/parse',
|
||||
auth: {
|
||||
challengeAdapter,
|
||||
},
|
||||
});
|
||||
|
||||
const user = new Parse.User();
|
||||
await user.save({ username: 'username', password: 'password' });
|
||||
|
||||
const result = await apolloClient.mutate({
|
||||
mutation: gql`
|
||||
mutation Challenge($input: ChallengeInput!) {
|
||||
challenge(input: $input) {
|
||||
clientMutationId
|
||||
challengeData
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
clientMutationId,
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
challengeData: {
|
||||
challengeAdapter: { someChallengeData: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const challengeCall = challengeAdapter.challenge.calls.argsFor(0);
|
||||
expect(challengeAdapter.challenge).toHaveBeenCalledTimes(1);
|
||||
expect(challengeCall[0]).toEqual({ someChallengeData: true });
|
||||
expect(challengeCall[1]).toEqual(undefined);
|
||||
expect(challengeCall[2]).toEqual(challengeAdapter);
|
||||
expect(challengeCall[3].object instanceof Parse.User).toBeTruthy();
|
||||
expect(challengeCall[3].original instanceof Parse.User).toBeTruthy();
|
||||
expect(challengeCall[3].isChallenge).toBeTruthy();
|
||||
expect(challengeCall[3].object.id).toEqual(user.id);
|
||||
expect(challengeCall[3].original.id).toEqual(user.id);
|
||||
expect(result.data.challenge.clientMutationId).toEqual(clientMutationId);
|
||||
expect(result.data.challenge.challengeData).toEqual({
|
||||
challengeAdapter: { someData: true },
|
||||
});
|
||||
|
||||
await expectAsync(
|
||||
apolloClient.mutate({
|
||||
mutation: gql`
|
||||
mutation Challenge($input: ChallengeInput!) {
|
||||
challenge(input: $input) {
|
||||
clientMutationId
|
||||
challengeData
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
clientMutationId,
|
||||
username: 'username',
|
||||
password: 'wrongPassword',
|
||||
challengeData: {
|
||||
challengeAdapter: { someChallengeData: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
).toBeRejected();
|
||||
});
|
||||
|
||||
it('should log the user in', async () => {
|
||||
parseServer = await global.reconfigureServer({
|
||||
publicServerURL: 'http://localhost:13377/parse',
|
||||
auth: {
|
||||
challengeAdapter,
|
||||
},
|
||||
});
|
||||
const clientMutationId = uuidv4();
|
||||
const user = new Parse.User();
|
||||
user.setUsername('user1');
|
||||
@@ -7326,7 +7162,6 @@ describe('ParseGraphQLServer', () => {
|
||||
viewer {
|
||||
sessionToken
|
||||
user {
|
||||
authDataResponse
|
||||
someField
|
||||
}
|
||||
}
|
||||
@@ -7338,7 +7173,6 @@ describe('ParseGraphQLServer', () => {
|
||||
clientMutationId,
|
||||
username: 'user1',
|
||||
password: 'user1',
|
||||
authData: { challengeAdapter: { token: true } },
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -7347,9 +7181,6 @@ describe('ParseGraphQLServer', () => {
|
||||
expect(result.data.logIn.viewer.sessionToken).toBeDefined();
|
||||
expect(result.data.logIn.viewer.user.someField).toEqual('someValue');
|
||||
expect(typeof result.data.logIn.viewer.sessionToken).toBe('string');
|
||||
expect(result.data.logIn.viewer.user.authDataResponse).toEqual({
|
||||
challengeAdapter: { someData: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('should log the user out', async () => {
|
||||
@@ -8324,21 +8155,19 @@ describe('ParseGraphQLServer', () => {
|
||||
const someClass = new Parse.Object('SomeClass');
|
||||
await someClass.save();
|
||||
|
||||
const roleACL = new Parse.ACL();
|
||||
roleACL.setPublicReadAccess(true);
|
||||
|
||||
const user = new Parse.User();
|
||||
user.set('username', 'username');
|
||||
user.set('password', 'password');
|
||||
user.setACL(roleACL);
|
||||
await user.signUp();
|
||||
|
||||
const user2 = new Parse.User();
|
||||
user2.set('username', 'username2');
|
||||
user2.set('password', 'password2');
|
||||
user2.setACL(roleACL);
|
||||
await user2.signUp();
|
||||
|
||||
const roleACL = new Parse.ACL();
|
||||
roleACL.setPublicReadAccess(true);
|
||||
|
||||
const role = new Parse.Role('aRole', roleACL);
|
||||
await role.save();
|
||||
|
||||
@@ -9301,7 +9130,7 @@ describe('ParseGraphQLServer', () => {
|
||||
expect(result6[0].node.name).toEqual('imACountry3');
|
||||
});
|
||||
|
||||
it('should support files', async () => {
|
||||
it_only_node_version('<17')('should support files', async () => {
|
||||
try {
|
||||
parseServer = await global.reconfigureServer({
|
||||
publicServerURL: 'http://localhost:13377/parse',
|
||||
@@ -9343,6 +9172,7 @@ describe('ParseGraphQLServer', () => {
|
||||
expect(res.status).toEqual(200);
|
||||
|
||||
const result = JSON.parse(await res.text());
|
||||
|
||||
expect(result.data.createFile.fileInfo.name).toEqual(
|
||||
jasmine.stringMatching(/_myFileName.txt$/)
|
||||
);
|
||||
@@ -9548,233 +9378,9 @@ describe('ParseGraphQLServer', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should support files on required file', async () => {
|
||||
try {
|
||||
parseServer = await global.reconfigureServer({
|
||||
publicServerURL: 'http://localhost:13377/parse',
|
||||
});
|
||||
const schemaController = await parseServer.config.databaseController.loadSchema();
|
||||
await schemaController.addClassIfNotExists('SomeClassWithRequiredFile', {
|
||||
someField: { type: 'File', required: true },
|
||||
});
|
||||
await resetGraphQLCache();
|
||||
await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
|
||||
it_only_node_version('<17')('should not upload if file is too large', async () => {
|
||||
parseGraphQLServer.parseServer.config.maxUploadSize = '1kb';
|
||||
|
||||
const body = new FormData();
|
||||
body.append(
|
||||
'operations',
|
||||
JSON.stringify({
|
||||
query: `
|
||||
mutation CreateSomeObject(
|
||||
$fields: CreateSomeClassWithRequiredFileFieldsInput
|
||||
) {
|
||||
createSomeClassWithRequiredFile(
|
||||
input: { fields: $fields }
|
||||
) {
|
||||
someClassWithRequiredFile {
|
||||
id
|
||||
someField {
|
||||
name
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
fields: {
|
||||
someField: { upload: null },
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
body.append('map', JSON.stringify({ 1: ['variables.fields.someField.upload'] }));
|
||||
body.append('1', 'My File Content', {
|
||||
filename: 'myFileName.txt',
|
||||
contentType: 'text/plain',
|
||||
});
|
||||
|
||||
const res = await fetch('http://localhost:13377/graphql', {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
expect(res.status).toEqual(200);
|
||||
const resText = await res.text();
|
||||
const result = JSON.parse(resText);
|
||||
expect(
|
||||
result.data.createSomeClassWithRequiredFile.someClassWithRequiredFile.someField.name
|
||||
).toEqual(jasmine.stringMatching(/_myFileName.txt$/));
|
||||
expect(
|
||||
result.data.createSomeClassWithRequiredFile.someClassWithRequiredFile.someField.url
|
||||
).toEqual(jasmine.stringMatching(/_myFileName.txt$/));
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
});
|
||||
|
||||
it('should support file upload for on fly creation through pointer and relation', async () => {
|
||||
parseServer = await global.reconfigureServer({
|
||||
publicServerURL: 'http://localhost:13377/parse',
|
||||
});
|
||||
const schema = new Parse.Schema('SomeClass');
|
||||
schema.addFile('someFileField');
|
||||
schema.addPointer('somePointerField', 'SomeClass');
|
||||
schema.addRelation('someRelationField', 'SomeClass');
|
||||
await schema.save();
|
||||
|
||||
const body = new FormData();
|
||||
body.append(
|
||||
'operations',
|
||||
JSON.stringify({
|
||||
query: `
|
||||
mutation UploadFiles(
|
||||
$fields: CreateSomeClassFieldsInput
|
||||
) {
|
||||
createSomeClass(
|
||||
input: { fields: $fields }
|
||||
) {
|
||||
someClass {
|
||||
id
|
||||
someFileField {
|
||||
name
|
||||
url
|
||||
}
|
||||
somePointerField {
|
||||
id
|
||||
someFileField {
|
||||
name
|
||||
url
|
||||
}
|
||||
}
|
||||
someRelationField {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
someFileField {
|
||||
name
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
fields: {
|
||||
someFileField: { upload: null },
|
||||
somePointerField: {
|
||||
createAndLink: {
|
||||
someFileField: { upload: null },
|
||||
},
|
||||
},
|
||||
someRelationField: {
|
||||
createAndAdd: [
|
||||
{
|
||||
someFileField: { upload: null },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
body.append(
|
||||
'map',
|
||||
JSON.stringify({
|
||||
1: ['variables.fields.someFileField.upload'],
|
||||
2: ['variables.fields.somePointerField.createAndLink.someFileField.upload'],
|
||||
3: ['variables.fields.someRelationField.createAndAdd.0.someFileField.upload'],
|
||||
})
|
||||
);
|
||||
body.append('1', 'My File Content someFileField', {
|
||||
filename: 'someFileField.txt',
|
||||
contentType: 'text/plain',
|
||||
});
|
||||
body.append('2', 'My File Content somePointerField', {
|
||||
filename: 'somePointerField.txt',
|
||||
contentType: 'text/plain',
|
||||
});
|
||||
body.append('3', 'My File Content someRelationField', {
|
||||
filename: 'someRelationField.txt',
|
||||
contentType: 'text/plain',
|
||||
});
|
||||
|
||||
const res = await fetch('http://localhost:13377/graphql', {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
expect(res.status).toEqual(200);
|
||||
const result = await res.json();
|
||||
expect(result.data.createSomeClass.someClass.someFileField.name).toEqual(
|
||||
jasmine.stringMatching(/_someFileField.txt$/)
|
||||
);
|
||||
expect(result.data.createSomeClass.someClass.somePointerField.someFileField.name).toEqual(
|
||||
jasmine.stringMatching(/_somePointerField.txt$/)
|
||||
);
|
||||
expect(
|
||||
result.data.createSomeClass.someClass.someRelationField.edges[0].node.someFileField.name
|
||||
).toEqual(jasmine.stringMatching(/_someRelationField.txt$/));
|
||||
});
|
||||
|
||||
it('should support files and add extension from mimetype', async () => {
|
||||
try {
|
||||
parseServer = await global.reconfigureServer({
|
||||
publicServerURL: 'http://localhost:13377/parse',
|
||||
});
|
||||
|
||||
const body = new FormData();
|
||||
body.append(
|
||||
'operations',
|
||||
JSON.stringify({
|
||||
query: `
|
||||
mutation CreateFile($input: CreateFileInput!) {
|
||||
createFile(input: $input) {
|
||||
fileInfo {
|
||||
name
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
upload: null,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
body.append('map', JSON.stringify({ 1: ['variables.input.upload'] }));
|
||||
body.append('1', 'My File Content', {
|
||||
// No extension, the system should add it from mimetype
|
||||
filename: 'myFileName',
|
||||
contentType: 'text/plain',
|
||||
});
|
||||
|
||||
const res = await fetch('http://localhost:13377/graphql', {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
|
||||
const result = JSON.parse(await res.text());
|
||||
expect(result.data.createFile.fileInfo.name).toEqual(
|
||||
jasmine.stringMatching(/_myFileName.txt$/)
|
||||
);
|
||||
expect(result.data.createFile.fileInfo.url).toEqual(
|
||||
jasmine.stringMatching(/_myFileName.txt$/)
|
||||
);
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
});
|
||||
|
||||
it('should not upload if file is too large', async () => {
|
||||
const body = new FormData();
|
||||
body.append(
|
||||
'operations',
|
||||
@@ -9799,7 +9405,6 @@ describe('ParseGraphQLServer', () => {
|
||||
body.append('map', JSON.stringify({ 1: ['variables.input.upload'] }));
|
||||
body.append(
|
||||
'1',
|
||||
// In this test file parse server is setup with 1kb limit
|
||||
Buffer.alloc(parseGraphQLServer._transformMaxUploadSizeToBytes('2kb'), 1),
|
||||
{
|
||||
filename: 'myFileName.txt',
|
||||
@@ -9814,10 +9419,8 @@ describe('ParseGraphQLServer', () => {
|
||||
});
|
||||
|
||||
const result = JSON.parse(await res.text());
|
||||
expect(res.status).toEqual(200);
|
||||
expect(result.errors[0].message).toEqual(
|
||||
'File truncated as it exceeds the 1024 byte size limit.'
|
||||
);
|
||||
expect(res.status).toEqual(500);
|
||||
expect(result.errors[0].message).toEqual('File size limit exceeded: 1024 bytes');
|
||||
});
|
||||
|
||||
it('should support object values', async () => {
|
||||
@@ -10841,9 +10444,6 @@ describe('ParseGraphQLServer', () => {
|
||||
const user = new Parse.User();
|
||||
user.setUsername('user1');
|
||||
user.setPassword('user1');
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
user.setACL(acl);
|
||||
await user.signUp();
|
||||
|
||||
await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
|
||||
|
||||
+2
-35
@@ -190,7 +190,7 @@ describe('Hooks', () => {
|
||||
|
||||
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(() => new Promise(resolve => setTimeout(resolve, 100)))
|
||||
.then(
|
||||
() => {
|
||||
return Parse.Hooks.createFunction('my_new_function', 'http://url.com');
|
||||
@@ -208,7 +208,7 @@ describe('Hooks', () => {
|
||||
expect(err).not.toBe(null);
|
||||
if (err) {
|
||||
expect(err.code).toBe(143);
|
||||
expect(err.message).toBe('function name: my_new_function already exists');
|
||||
expect(err.message).toBe('function name: my_new_function already exits');
|
||||
}
|
||||
return Parse.Hooks.removeFunction('my_new_function');
|
||||
}
|
||||
@@ -694,36 +694,3 @@ describe('triggers', () => {
|
||||
expect(req.context).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizing names', () => {
|
||||
const invalidNames = [
|
||||
`test'%3bdeclare%20@q%20varchar(99)%3bset%20@q%3d'%5c%5cxxxxxxxxxxxxxxx.yyyyy'%2b'fy.com%5cxus'%3b%20exec%20master.dbo.xp_dirtree%20@q%3b--%20`,
|
||||
`test.function.name`,
|
||||
];
|
||||
|
||||
it('should not crash server and return error on invalid Cloud Function name', async () => {
|
||||
for (const invalidName of invalidNames) {
|
||||
let error;
|
||||
try {
|
||||
await Parse.Cloud.run(invalidName);
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toMatch(/Invalid function/);
|
||||
}
|
||||
});
|
||||
|
||||
it('should not crash server and return error on invalid Cloud Job name', async () => {
|
||||
for (const invalidName of invalidNames) {
|
||||
let error;
|
||||
try {
|
||||
await Parse.Cloud.startJob(invalidName);
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toMatch(/Invalid job/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+185
-217
@@ -1,23 +1,21 @@
|
||||
'use strict';
|
||||
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 validatorFail = () => {
|
||||
throw 'you are not authorized';
|
||||
};
|
||||
|
||||
describe('ParseLiveQuery', function () {
|
||||
beforeEach(() => {
|
||||
Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
|
||||
});
|
||||
afterEach(async () => {
|
||||
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
|
||||
await client.close();
|
||||
});
|
||||
it('access user on onLiveQueryEvent disconnect', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
|
||||
const requestedUser = new Parse.User();
|
||||
requestedUser.setUsername('username');
|
||||
requestedUser.setPassword('password');
|
||||
@@ -34,10 +32,18 @@ describe('ParseLiveQuery', function () {
|
||||
const query = new Parse.Query(TestObject);
|
||||
await query.subscribe();
|
||||
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
|
||||
await client.close();
|
||||
client.close();
|
||||
});
|
||||
|
||||
it('can subscribe to query', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
|
||||
@@ -103,6 +109,14 @@ describe('ParseLiveQuery', function () {
|
||||
});
|
||||
|
||||
it('expect afterEvent payload', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
|
||||
@@ -122,6 +136,14 @@ describe('ParseLiveQuery', function () {
|
||||
});
|
||||
|
||||
it('expect afterEvent enter', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
Parse.Cloud.afterLiveQueryEvent('TestObject', req => {
|
||||
expect(req.event).toBe('enter');
|
||||
expect(req.user).toBeUndefined();
|
||||
@@ -145,6 +167,14 @@ describe('ParseLiveQuery', function () {
|
||||
});
|
||||
|
||||
it('expect afterEvent leave', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
Parse.Cloud.afterLiveQueryEvent('TestObject', req => {
|
||||
expect(req.event).toBe('leave');
|
||||
expect(req.user).toBeUndefined();
|
||||
@@ -169,6 +199,14 @@ describe('ParseLiveQuery', function () {
|
||||
});
|
||||
|
||||
it('expect afterEvent delete', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
Parse.Cloud.afterLiveQueryEvent('TestObject', req => {
|
||||
expect(req.event).toBe('delete');
|
||||
expect(req.user).toBeUndefined();
|
||||
@@ -316,18 +354,30 @@ describe('ParseLiveQuery', function () {
|
||||
);
|
||||
});
|
||||
|
||||
it('can handle afterEvent sendEvent to false', async () => {
|
||||
it('can handle afterEvent sendEvent to false', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
const promise = resolvingPromise();
|
||||
|
||||
Parse.Cloud.afterLiveQueryEvent('TestObject', req => {
|
||||
const current = req.object;
|
||||
const original = req.original;
|
||||
|
||||
setTimeout(() => {
|
||||
done();
|
||||
}, 2000);
|
||||
|
||||
if (current.get('foo') != original.get('foo')) {
|
||||
req.sendEvent = false;
|
||||
}
|
||||
promise.resolve();
|
||||
});
|
||||
|
||||
const query = new Parse.Query(TestObject);
|
||||
@@ -341,49 +391,6 @@ describe('ParseLiveQuery', function () {
|
||||
});
|
||||
object.set({ foo: 'bar' });
|
||||
await object.save();
|
||||
await promise;
|
||||
});
|
||||
|
||||
it('can handle live query with fields', async () => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['Test'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
});
|
||||
const query = new Parse.Query('Test');
|
||||
query.watch('yolo');
|
||||
const subscription = await query.subscribe();
|
||||
const spy = {
|
||||
create(obj) {
|
||||
if (!obj.get('yolo')) {
|
||||
fail('create should not have been called');
|
||||
}
|
||||
},
|
||||
update(object, original) {
|
||||
if (object.get('yolo') === original.get('yolo')) {
|
||||
fail('create should not have been called');
|
||||
}
|
||||
},
|
||||
};
|
||||
const createSpy = spyOn(spy, 'create').and.callThrough();
|
||||
const updateSpy = spyOn(spy, 'update').and.callThrough();
|
||||
subscription.on('create', spy.create);
|
||||
subscription.on('update', spy.update);
|
||||
const obj = new Parse.Object('Test');
|
||||
obj.set('foo', 'bar');
|
||||
await obj.save();
|
||||
obj.set('foo', 'xyz');
|
||||
obj.set('yolo', 'xyz');
|
||||
await obj.save();
|
||||
const obj2 = new Parse.Object('Test');
|
||||
obj2.set('foo', 'bar');
|
||||
obj2.set('yolo', 'bar');
|
||||
await obj2.save();
|
||||
obj2.set('foo', 'bart');
|
||||
await obj2.save();
|
||||
expect(createSpy).toHaveBeenCalledTimes(1);
|
||||
expect(updateSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('can handle afterEvent set pointers', async done => {
|
||||
@@ -462,67 +469,76 @@ describe('ParseLiveQuery', function () {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
const hooks = {
|
||||
beforeSubscribe(req) {
|
||||
expect(req.op).toBe('subscribe');
|
||||
expect(req.requestId).toBe(1);
|
||||
expect(req.query).toBeDefined();
|
||||
expect(req.user).toBeUndefined();
|
||||
},
|
||||
beforeConnect(req) {
|
||||
expect(req.event).toBe('connect');
|
||||
expect(req.clients).toBe(0);
|
||||
expect(req.subscriptions).toBe(0);
|
||||
expect(req.useMasterKey).toBe(false);
|
||||
expect(req.installationId).toBeDefined();
|
||||
expect(req.user).toBeUndefined();
|
||||
expect(req.client).toBeDefined();
|
||||
},
|
||||
};
|
||||
spyOn(hooks, 'beforeSubscribe').and.callThrough();
|
||||
spyOn(hooks, 'beforeConnect').and.callThrough();
|
||||
Parse.Cloud.beforeSubscribe('TestObject', hooks.beforeSubscribe);
|
||||
Parse.Cloud.beforeConnect(hooks.beforeConnect);
|
||||
|
||||
Parse.Cloud.beforeSubscribe('TestObject', req => {
|
||||
expect(req.op).toBe('subscribe');
|
||||
expect(req.requestId).toBe(1);
|
||||
expect(req.query).toBeDefined();
|
||||
expect(req.user).toBeUndefined();
|
||||
});
|
||||
|
||||
Parse.Cloud.beforeConnect(req => {
|
||||
expect(req.event).toBe('connect');
|
||||
expect(req.clients).toBe(0);
|
||||
expect(req.subscriptions).toBe(0);
|
||||
expect(req.useMasterKey).toBe(false);
|
||||
expect(req.installationId).toBeDefined();
|
||||
expect(req.user).toBeUndefined();
|
||||
expect(req.client).toBeDefined();
|
||||
});
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.equalTo('objectId', object.id);
|
||||
const subscription = await query.subscribe();
|
||||
subscription.on('update', object => {
|
||||
expect(object.get('foo')).toBe('bar');
|
||||
expect(hooks.beforeConnect).toHaveBeenCalled();
|
||||
expect(hooks.beforeSubscribe).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
object.set({ foo: 'bar' });
|
||||
await object.save();
|
||||
});
|
||||
|
||||
it('can handle beforeConnect validation function', async () => {
|
||||
it('can handle beforeConnect validation function', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
|
||||
Parse.Cloud.beforeConnect(() => {}, validatorFail);
|
||||
let complete = false;
|
||||
Parse.LiveQuery.on('error', error => {
|
||||
Parse.LiveQuery.removeAllListeners('error');
|
||||
if (complete) {
|
||||
return;
|
||||
}
|
||||
complete = true;
|
||||
expect(error).toBe('you are not authorized');
|
||||
done();
|
||||
});
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.equalTo('objectId', object.id);
|
||||
await expectAsync(query.subscribe()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.VALIDATION_ERROR, 'you are not authorized')
|
||||
);
|
||||
await query.subscribe();
|
||||
});
|
||||
|
||||
it('can handle beforeSubscribe validation function', async () => {
|
||||
it('can handle beforeSubscribe validation function', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
@@ -530,9 +546,11 @@ describe('ParseLiveQuery', function () {
|
||||
Parse.Cloud.beforeSubscribe(TestObject, () => {}, validatorFail);
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.equalTo('objectId', object.id);
|
||||
await expectAsync(query.subscribe()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.VALIDATION_ERROR, 'you are not authorized')
|
||||
);
|
||||
const subscription = await query.subscribe();
|
||||
subscription.on('error', error => {
|
||||
expect(error).toBe('you are not authorized');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can handle afterEvent validation function', async done => {
|
||||
@@ -558,12 +576,14 @@ describe('ParseLiveQuery', function () {
|
||||
await object.save();
|
||||
});
|
||||
|
||||
it('can handle beforeConnect error', async () => {
|
||||
it('can handle beforeConnect error', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
@@ -571,9 +591,14 @@ describe('ParseLiveQuery', function () {
|
||||
Parse.Cloud.beforeConnect(() => {
|
||||
throw new Error('You shall not pass!');
|
||||
});
|
||||
Parse.LiveQuery.on('error', error => {
|
||||
Parse.LiveQuery.removeAllListeners('error');
|
||||
expect(error).toBe('You shall not pass!');
|
||||
done();
|
||||
});
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.equalTo('objectId', object.id);
|
||||
await expectAsync(query.subscribe()).toBeRejectedWith(new Error('You shall not pass!'));
|
||||
await query.subscribe();
|
||||
});
|
||||
|
||||
it('can log on beforeConnect throw', async () => {
|
||||
@@ -582,6 +607,8 @@ describe('ParseLiveQuery', function () {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
const logger = require('../lib/logger').logger;
|
||||
@@ -593,20 +620,22 @@ describe('ParseLiveQuery', function () {
|
||||
foo.bar();
|
||||
/* eslint-enable no-undef */
|
||||
});
|
||||
await expectAsync(new Parse.Query(TestObject).subscribe()).toBeRejectedWith(
|
||||
new Error('foo is not defined')
|
||||
);
|
||||
new Parse.Query(TestObject).subscribe();
|
||||
await new Promise(resolve => Parse.LiveQuery.on('error', resolve));
|
||||
Parse.LiveQuery.removeAllListeners('error');
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
`Failed running beforeConnect for session ${token} with:\n Error: {"message":"foo is not defined","code":141}`
|
||||
);
|
||||
});
|
||||
|
||||
it('can handle beforeSubscribe error', async () => {
|
||||
it('can handle beforeSubscribe error', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
@@ -614,9 +643,17 @@ describe('ParseLiveQuery', function () {
|
||||
Parse.Cloud.beforeSubscribe(TestObject, () => {
|
||||
throw new Error('You shall not subscribe!');
|
||||
});
|
||||
Parse.LiveQuery.on('error', error => {
|
||||
expect(error).toBe('You shall not subscribe!');
|
||||
});
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.equalTo('objectId', object.id);
|
||||
await expectAsync(query.subscribe()).toBeRejectedWith(new Error('You shall not subscribe!'));
|
||||
const subscription = await query.subscribe();
|
||||
subscription.on('error', error => {
|
||||
Parse.LiveQuery.removeAllListeners('error');
|
||||
expect(error).toBe('You shall not subscribe!');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can log on beforeSubscribe error', async () => {
|
||||
@@ -625,6 +662,8 @@ describe('ParseLiveQuery', function () {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
const logger = require('../lib/logger').logger;
|
||||
@@ -637,7 +676,8 @@ describe('ParseLiveQuery', function () {
|
||||
});
|
||||
|
||||
const query = new Parse.Query(TestObject);
|
||||
await expectAsync(query.subscribe()).toBeRejectedWith(new Error('foo is not defined'));
|
||||
const subscription = await query.subscribe();
|
||||
await new Promise(resolve => subscription.on('error', resolve));
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
`Failed running beforeSubscribe on TestObject for session undefined with:\n Error: {"message":"foo is not defined","code":141}`
|
||||
@@ -650,35 +690,29 @@ describe('ParseLiveQuery', function () {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
const hook = {
|
||||
beforeSubscribe(request) {
|
||||
request.query.equalTo('yolo', 'abc');
|
||||
},
|
||||
};
|
||||
spyOn(hook, 'beforeSubscribe').and.callThrough();
|
||||
Parse.Cloud.beforeSubscribe('TestObject', hook.beforeSubscribe);
|
||||
Parse.Cloud.beforeSubscribe(TestObject, request => {
|
||||
const query = request.query;
|
||||
query.equalTo('yolo', 'abc');
|
||||
});
|
||||
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
|
||||
const query = new Parse.Query('TestObject');
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.equalTo('objectId', object.id);
|
||||
const subscription = await query.subscribe();
|
||||
|
||||
subscription.on('update', () => {
|
||||
fail('beforeSubscribe should restrict subscription');
|
||||
fail();
|
||||
});
|
||||
subscription.on('enter', object => {
|
||||
if (object.get('yolo') === 'abc') {
|
||||
done();
|
||||
} else {
|
||||
fail('beforeSubscribe should restrict queries');
|
||||
}
|
||||
});
|
||||
object.set({ yolo: 'bar' });
|
||||
object.set({ foo: 'bar' });
|
||||
await object.save();
|
||||
object.set({ yolo: 'abc' });
|
||||
await object.save();
|
||||
expect(hook.beforeSubscribe).toHaveBeenCalled();
|
||||
setTimeout(async () => {
|
||||
done();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
it('can return a new beforeSubscribe query', async done => {
|
||||
@@ -710,6 +744,14 @@ describe('ParseLiveQuery', function () {
|
||||
});
|
||||
|
||||
it('can handle select beforeSubscribe query', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
Parse.Cloud.beforeSubscribe(TestObject, request => {
|
||||
const query = request.query;
|
||||
query.select('yolo');
|
||||
@@ -788,7 +830,7 @@ describe('ParseLiveQuery', function () {
|
||||
object.set({ foo: 'bar' });
|
||||
await object.save();
|
||||
await object.destroy();
|
||||
await sleep(200);
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
for (const key in calls) {
|
||||
expect(calls[key]).toHaveBeenCalled();
|
||||
}
|
||||
@@ -822,9 +864,9 @@ describe('ParseLiveQuery', function () {
|
||||
object.set({ foo: 'bar' });
|
||||
await object.save(null, { useMasterKey: true });
|
||||
role.getUsers().add(user);
|
||||
await sleep(1000);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await role.save();
|
||||
await sleep(1000);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
object.set('foo', 'yolo');
|
||||
await Promise.all([
|
||||
new Promise(resolve => {
|
||||
@@ -855,6 +897,7 @@ describe('ParseLiveQuery', function () {
|
||||
const subscription = await query.subscribe();
|
||||
|
||||
subscription.on('create', async obj => {
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
expect(obj.get('user').id).toBe(user.id);
|
||||
expect(obj.get('createdWith')).toEqual({ action: 'login', authProvider: 'password' });
|
||||
expect(obj.get('expiresAt')).toBeInstanceOf(Date);
|
||||
@@ -867,15 +910,26 @@ describe('ParseLiveQuery', function () {
|
||||
await Parse.User.logIn('username', 'password');
|
||||
});
|
||||
|
||||
it('prevent liveQuery on Session class when not logged in', async () => {
|
||||
it('prevent liveQuery on Session class when not logged in', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: [Parse.Session],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
Parse.LiveQuery.on('error', error => {
|
||||
expect(error).toBe('Invalid session token');
|
||||
});
|
||||
const query = new Parse.Query(Parse.Session);
|
||||
await expectAsync(query.subscribe()).toBeRejectedWith(new Error('Invalid session token'));
|
||||
const subscription = await query.subscribe();
|
||||
subscription.on('error', error => {
|
||||
Parse.LiveQuery.removeAllListeners('error');
|
||||
expect(error).toBe('Invalid session token');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('handle invalid websocket payload length', async done => {
|
||||
@@ -923,7 +977,6 @@ describe('ParseLiveQuery', function () {
|
||||
};
|
||||
|
||||
await reconfigureServer({
|
||||
maintenanceKey: 'test2',
|
||||
liveQuery: {
|
||||
classNames: [Parse.User],
|
||||
},
|
||||
@@ -945,14 +998,9 @@ describe('ParseLiveQuery', function () {
|
||||
.signUp()
|
||||
.then(() => {
|
||||
const config = Config.get('test');
|
||||
return config.database.find(
|
||||
'_User',
|
||||
{
|
||||
username: 'zxcv',
|
||||
},
|
||||
{},
|
||||
Auth.maintenance(config)
|
||||
);
|
||||
return config.database.find('_User', {
|
||||
username: 'zxcv',
|
||||
});
|
||||
})
|
||||
.then(async results => {
|
||||
const foundUser = results[0];
|
||||
@@ -1028,9 +1076,6 @@ describe('ParseLiveQuery', function () {
|
||||
user.setUsername('username');
|
||||
user.setPassword('password');
|
||||
user.set('foo', 'bar');
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
user.setACL(acl);
|
||||
|
||||
const query = new Parse.Query(Parse.User);
|
||||
query.equalTo('foo', 'bar');
|
||||
@@ -1145,89 +1190,12 @@ describe('ParseLiveQuery', function () {
|
||||
await object.save();
|
||||
});
|
||||
|
||||
it('does shutdown liveQuery server', async () => {
|
||||
await reconfigureServer({ appId: 'test_app_id' });
|
||||
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,
|
||||
};
|
||||
if (process.env.PARSE_SERVER_TEST_DB === 'postgres') {
|
||||
config.databaseAdapter = new databaseAdapter.constructor({
|
||||
uri: databaseURI,
|
||||
collectionPrefix: 'test_',
|
||||
});
|
||||
config.filesAdapter = defaultConfiguration.filesAdapter;
|
||||
}
|
||||
const server = await ParseServer.startApp(config);
|
||||
afterEach(async function (done) {
|
||||
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
|
||||
client.serverURL = 'ws://localhost:1345/1';
|
||||
const query = await new Parse.Query('Yolo').subscribe();
|
||||
await Promise.all([
|
||||
server.handleShutdown(),
|
||||
new Promise(resolve => query.on('close', resolve)),
|
||||
]);
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
expect(server.liveQueryServer.server.address()).toBeNull();
|
||||
expect(server.liveQueryServer.subscriber.isOpen).toBeFalse();
|
||||
await new Promise(resolve => server.server.close(resolve));
|
||||
});
|
||||
|
||||
it('prevent afterSave trigger if not exists', async () => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
spyOn(triggers, 'maybeRunTrigger').and.callThrough();
|
||||
const object1 = new TestObject();
|
||||
const object2 = new TestObject();
|
||||
const object3 = new TestObject();
|
||||
await Parse.Object.saveAll([object1, object2, object3]);
|
||||
|
||||
expect(triggers.maybeRunTrigger).toHaveBeenCalledTimes(0);
|
||||
expect(object1.id).toBeDefined();
|
||||
expect(object2.id).toBeDefined();
|
||||
expect(object3.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('triggers query event with constraint not equal to null', async () => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
const spy = {
|
||||
create(obj) {
|
||||
expect(obj.attributes.foo).toEqual('bar');
|
||||
},
|
||||
};
|
||||
const createSpy = spyOn(spy, 'create');
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.notEqualTo('foo', null);
|
||||
const subscription = await query.subscribe();
|
||||
subscription.on('create', spy.create);
|
||||
|
||||
const object1 = new TestObject();
|
||||
object1.set('foo', 'bar');
|
||||
await object1.save();
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
expect(createSpy).toHaveBeenCalledTimes(1);
|
||||
client.close();
|
||||
// Wait for live query client to disconnect
|
||||
setTimeout(() => {
|
||||
done();
|
||||
}, 1000);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
if (process.env.PARSE_SERVER_TEST_CACHE === 'redis') {
|
||||
describe('ParseLiveQuery redis', () => {
|
||||
afterEach(async () => {
|
||||
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
|
||||
client.close();
|
||||
});
|
||||
it('can connect', async () => {
|
||||
await reconfigureServer({
|
||||
appId: 'redis_live_query',
|
||||
startLiveQueryServer: true,
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
redisURL: 'redis://localhost:6379',
|
||||
},
|
||||
liveQueryServerOptions: {
|
||||
redisURL: 'redis://localhost:6379',
|
||||
},
|
||||
});
|
||||
const subscription = await new Parse.Query('TestObject').subscribe();
|
||||
const [object] = await Promise.all([
|
||||
new Parse.Object('TestObject').save(),
|
||||
new Promise(resolve =>
|
||||
subscription.on('create', () => {
|
||||
resolve();
|
||||
})
|
||||
),
|
||||
]);
|
||||
await Promise.all([
|
||||
new Promise(resolve =>
|
||||
subscription.on('delete', () => {
|
||||
resolve();
|
||||
})
|
||||
),
|
||||
object.destroy(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('can call connect twice', async () => {
|
||||
const server = await reconfigureServer({
|
||||
appId: 'redis_live_query',
|
||||
startLiveQueryServer: true,
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
redisURL: 'redis://localhost:6379',
|
||||
},
|
||||
liveQueryServerOptions: {
|
||||
redisURL: 'redis://localhost:6379',
|
||||
},
|
||||
});
|
||||
expect(server.config.liveQueryController.liveQueryPublisher.parsePublisher.isOpen).toBeTrue();
|
||||
await server.config.liveQueryController.connect();
|
||||
expect(server.config.liveQueryController.liveQueryPublisher.parsePublisher.isOpen).toBeTrue();
|
||||
expect(server.liveQueryServer.subscriber.isOpen).toBe(true);
|
||||
await server.liveQueryServer.connect();
|
||||
expect(server.liveQueryServer.subscriber.isOpen).toBe(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -94,30 +94,29 @@ describe('ParseLiveQueryServer', function () {
|
||||
expect(parseLiveQueryServer.subscriptions.size).toBe(0);
|
||||
});
|
||||
|
||||
it('can be initialized from ParseServer', async () => {
|
||||
it('can be initialized from ParseServer', function () {
|
||||
const httpServer = {};
|
||||
const parseLiveQueryServer = await ParseServer.createLiveQueryServer(httpServer, {});
|
||||
const parseLiveQueryServer = ParseServer.createLiveQueryServer(httpServer, {});
|
||||
|
||||
expect(parseLiveQueryServer.clientId).toBeUndefined();
|
||||
expect(parseLiveQueryServer.clients.size).toBe(0);
|
||||
expect(parseLiveQueryServer.subscriptions.size).toBe(0);
|
||||
});
|
||||
|
||||
it('can be initialized from ParseServer without httpServer', async () => {
|
||||
const parseLiveQueryServer = await ParseServer.createLiveQueryServer(undefined, {
|
||||
it('can be initialized from ParseServer without httpServer', function (done) {
|
||||
const parseLiveQueryServer = ParseServer.createLiveQueryServer(undefined, {
|
||||
port: 22345,
|
||||
});
|
||||
|
||||
expect(parseLiveQueryServer.clientId).toBeUndefined();
|
||||
expect(parseLiveQueryServer.clients.size).toBe(0);
|
||||
expect(parseLiveQueryServer.subscriptions.size).toBe(0);
|
||||
await new Promise(resolve => parseLiveQueryServer.server.close(resolve));
|
||||
parseLiveQueryServer.server.close(done);
|
||||
});
|
||||
|
||||
describe_only_db('mongo')('initialization', () => {
|
||||
beforeEach(() => reconfigureServer({ appId: 'mongo_init_test' }));
|
||||
it('can be initialized through ParseServer without liveQueryServerOptions', async () => {
|
||||
const parseServer = await ParseServer.startApp({
|
||||
it('can be initialized through ParseServer without liveQueryServerOptions', function (done) {
|
||||
const parseServer = ParseServer.start({
|
||||
appId: 'hello',
|
||||
masterKey: 'world',
|
||||
port: 22345,
|
||||
@@ -127,14 +126,19 @@ describe('ParseLiveQueryServer', function () {
|
||||
classNames: ['Yolo'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
serverStartComplete: () => {
|
||||
expect(parseServer.liveQueryServer).not.toBeUndefined();
|
||||
expect(parseServer.liveQueryServer.server).toBe(parseServer.server);
|
||||
parseServer.server.close(async () => {
|
||||
await reconfigureServer();
|
||||
done();
|
||||
});
|
||||
},
|
||||
});
|
||||
expect(parseServer.liveQueryServer).not.toBeUndefined();
|
||||
expect(parseServer.liveQueryServer.server).toBe(parseServer.server);
|
||||
await new Promise(resolve => parseServer.server.close(resolve));
|
||||
});
|
||||
|
||||
it('can be initialized through ParseServer with liveQueryServerOptions', async () => {
|
||||
const parseServer = await ParseServer.startApp({
|
||||
it('can be initialized through ParseServer with liveQueryServerOptions', function (done) {
|
||||
const parseServer = ParseServer.start({
|
||||
appId: 'hello',
|
||||
masterKey: 'world',
|
||||
port: 22346,
|
||||
@@ -146,10 +150,17 @@ describe('ParseLiveQueryServer', function () {
|
||||
liveQueryServerOptions: {
|
||||
port: 22347,
|
||||
},
|
||||
serverStartComplete: () => {
|
||||
expect(parseServer.liveQueryServer).not.toBeUndefined();
|
||||
expect(parseServer.liveQueryServer.server).not.toBe(parseServer.server);
|
||||
parseServer.liveQueryServer.server.close(
|
||||
parseServer.server.close.bind(parseServer.server, async () => {
|
||||
await reconfigureServer();
|
||||
done();
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
expect(parseServer.liveQueryServer).not.toBeUndefined();
|
||||
expect(parseServer.liveQueryServer.server).not.toBe(parseServer.server);
|
||||
await new Promise(resolve => parseServer.server.close(resolve));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -295,7 +306,7 @@ describe('ParseLiveQueryServer', function () {
|
||||
where: {
|
||||
key: 'value',
|
||||
},
|
||||
keys: ['test'],
|
||||
fields: ['test'],
|
||||
};
|
||||
const requestId = 2;
|
||||
const request = {
|
||||
@@ -332,7 +343,7 @@ describe('ParseLiveQueryServer', function () {
|
||||
where: {
|
||||
key: 'value',
|
||||
},
|
||||
keys: ['test'],
|
||||
fields: ['test'],
|
||||
};
|
||||
const requestId = 2;
|
||||
const request = {
|
||||
@@ -356,7 +367,7 @@ describe('ParseLiveQueryServer', function () {
|
||||
// Make sure we add subscriptionInfo to the client
|
||||
const args = client.addSubscriptionInfo.calls.first().args;
|
||||
expect(args[0]).toBe(requestId);
|
||||
expect(args[1].keys).toBe(query.keys);
|
||||
expect(args[1].fields).toBe(query.fields);
|
||||
expect(args[1].sessionToken).toBe(request.sessionToken);
|
||||
// Make sure we send subscribe response to the client
|
||||
expect(client.pushSubscribe).toHaveBeenCalledWith(requestId);
|
||||
@@ -379,7 +390,7 @@ describe('ParseLiveQueryServer', function () {
|
||||
where: {
|
||||
key: 'value',
|
||||
},
|
||||
keys: ['test'],
|
||||
fields: ['test'],
|
||||
};
|
||||
await addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket, query);
|
||||
// Add subscription for mock client 2
|
||||
@@ -391,7 +402,7 @@ describe('ParseLiveQueryServer', function () {
|
||||
where: {
|
||||
key: 'value',
|
||||
},
|
||||
keys: ['testAgain'],
|
||||
fields: ['testAgain'],
|
||||
};
|
||||
const requestIdAgain = 1;
|
||||
await addMockSubscription(
|
||||
@@ -417,7 +428,7 @@ describe('ParseLiveQueryServer', function () {
|
||||
// Make sure we add subscriptionInfo to the client 2
|
||||
args = clientAgain.addSubscriptionInfo.calls.mostRecent().args;
|
||||
expect(args[0]).toBe(requestIdAgain);
|
||||
expect(args[1].keys).toBe(queryAgain.keys);
|
||||
expect(args[1].fields).toBe(queryAgain.fields);
|
||||
});
|
||||
|
||||
it('can handle unsubscribe command without clientId', function () {
|
||||
@@ -755,49 +766,6 @@ describe('ParseLiveQueryServer', function () {
|
||||
parseLiveQueryServer._onAfterSave(message);
|
||||
});
|
||||
|
||||
it('sends correct object for dates', async () => {
|
||||
jasmine.restoreLibrary('../lib/LiveQuery/QueryTools', 'matchesQuery');
|
||||
|
||||
const parseLiveQueryServer = new ParseLiveQueryServer({});
|
||||
|
||||
const date = new Date();
|
||||
const message = {
|
||||
currentParseObject: {
|
||||
date: { __type: 'Date', iso: date.toISOString() },
|
||||
__type: 'Object',
|
||||
key: 'value',
|
||||
className: testClassName,
|
||||
},
|
||||
};
|
||||
// Add mock client
|
||||
const clientId = 1;
|
||||
const client = addMockClient(parseLiveQueryServer, clientId);
|
||||
|
||||
const requestId2 = 2;
|
||||
|
||||
await addMockSubscription(parseLiveQueryServer, clientId, requestId2);
|
||||
|
||||
parseLiveQueryServer._matchesACL = function () {
|
||||
return Promise.resolve(true);
|
||||
};
|
||||
|
||||
parseLiveQueryServer._inflateParseObject(message);
|
||||
parseLiveQueryServer._onAfterSave(message);
|
||||
|
||||
// Make sure we send leave and enter command to client
|
||||
await timeout();
|
||||
|
||||
expect(client.pushCreate).toHaveBeenCalledWith(
|
||||
requestId2,
|
||||
{
|
||||
className: 'TestObject',
|
||||
key: 'value',
|
||||
date: { __type: 'Date', iso: date.toISOString() },
|
||||
},
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
it('can handle object save command which does not match any subscription', async done => {
|
||||
const parseLiveQueryServer = new ParseLiveQueryServer({});
|
||||
// Make mock request message
|
||||
@@ -1081,7 +1049,7 @@ describe('ParseLiveQueryServer', function () {
|
||||
done();
|
||||
});
|
||||
|
||||
it('can handle create command with keys', async done => {
|
||||
it('can handle create command with fields', async done => {
|
||||
jasmine.restoreLibrary('../lib/LiveQuery/Client', 'Client');
|
||||
const Client = require('../lib/LiveQuery/Client').Client;
|
||||
const parseLiveQueryServer = new ParseLiveQueryServer({});
|
||||
@@ -1104,7 +1072,7 @@ describe('ParseLiveQueryServer', function () {
|
||||
where: {
|
||||
key: 'value',
|
||||
},
|
||||
keys: ['test'],
|
||||
fields: ['test'],
|
||||
};
|
||||
await addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket, query);
|
||||
// Mock _matchesSubscription to return matching
|
||||
@@ -1131,61 +1099,6 @@ describe('ParseLiveQueryServer', function () {
|
||||
done();
|
||||
});
|
||||
|
||||
it('can handle create command with watch', async () => {
|
||||
jasmine.restoreLibrary('../lib/LiveQuery/Client', 'Client');
|
||||
const Client = require('../lib/LiveQuery/Client').Client;
|
||||
const parseLiveQueryServer = new ParseLiveQueryServer({});
|
||||
// Make mock request message
|
||||
const message = generateMockMessage();
|
||||
|
||||
const clientId = 1;
|
||||
const parseWebSocket = {
|
||||
clientId,
|
||||
send: jasmine.createSpy('send'),
|
||||
};
|
||||
const client = new Client(clientId, parseWebSocket);
|
||||
spyOn(client, 'pushCreate').and.callThrough();
|
||||
parseLiveQueryServer.clients.set(clientId, client);
|
||||
|
||||
// Add mock subscription
|
||||
const requestId = 2;
|
||||
const query = {
|
||||
className: testClassName,
|
||||
where: {
|
||||
key: 'value',
|
||||
},
|
||||
watch: ['yolo'],
|
||||
};
|
||||
await addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket, query);
|
||||
// Mock _matchesSubscription to return matching
|
||||
parseLiveQueryServer._matchesSubscription = function (parseObject) {
|
||||
if (!parseObject) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
parseLiveQueryServer._matchesACL = function () {
|
||||
return Promise.resolve(true);
|
||||
};
|
||||
|
||||
parseLiveQueryServer._onAfterSave(message);
|
||||
|
||||
// Make sure we send create command to client
|
||||
await timeout();
|
||||
|
||||
expect(client.pushCreate).not.toHaveBeenCalled();
|
||||
|
||||
message.currentParseObject.set('yolo', 'test');
|
||||
parseLiveQueryServer._onAfterSave(message);
|
||||
|
||||
await timeout();
|
||||
|
||||
const args = parseWebSocket.send.calls.mostRecent().args;
|
||||
const toSend = JSON.parse(args[0]);
|
||||
expect(toSend.object).toBeDefined();
|
||||
expect(toSend.original).toBeUndefined();
|
||||
});
|
||||
|
||||
it('can match subscription for null or undefined parse object', function () {
|
||||
const parseLiveQueryServer = new ParseLiveQueryServer({});
|
||||
// Make mock subscription
|
||||
@@ -1633,6 +1546,35 @@ describe('ParseLiveQueryServer', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('matches CLP when find is restricted to userIds', done => {
|
||||
const parseLiveQueryServer = new ParseLiveQueryServer({});
|
||||
const acl = new Parse.ACL();
|
||||
acl.setReadAccess(testUserId, true);
|
||||
// Mock sessionTokenCache will return false when sessionToken is undefined
|
||||
const client = {
|
||||
sessionToken: 'sessionToken',
|
||||
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
|
||||
sessionToken: 'userId',
|
||||
}),
|
||||
};
|
||||
const requestId = 0;
|
||||
|
||||
parseLiveQueryServer
|
||||
._matchesCLP(
|
||||
{
|
||||
find: { userId: true },
|
||||
},
|
||||
{ className: 'Yolo' },
|
||||
client,
|
||||
requestId,
|
||||
'find'
|
||||
)
|
||||
.then(isMatched => {
|
||||
expect(isMatched).toBe(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('matches CLP when find is restricted to userIds', done => {
|
||||
const parseLiveQueryServer = new ParseLiveQueryServer({});
|
||||
const acl = new Parse.ACL();
|
||||
@@ -1880,7 +1822,7 @@ describe('ParseLiveQueryServer', function () {
|
||||
where: {
|
||||
key: 'value',
|
||||
},
|
||||
keys: ['test'],
|
||||
fields: ['test'],
|
||||
};
|
||||
}
|
||||
const request = {
|
||||
@@ -1904,7 +1846,6 @@ describe('ParseLiveQueryServer', function () {
|
||||
} else {
|
||||
subscription.clientRequestIds = new Map([[clientId, [requestId]]]);
|
||||
}
|
||||
subscription.query = query.where;
|
||||
return subscription;
|
||||
}
|
||||
|
||||
|
||||
@@ -568,70 +568,6 @@ describe('Parse.Object testing', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it_only_db('mongo')('can increment array nested fields', async () => {
|
||||
const obj = new TestObject();
|
||||
obj.set('items', [ { value: 'a', count: 5 }, { value: 'b', count: 1 } ]);
|
||||
await obj.save();
|
||||
obj.increment('items.0.count', 15);
|
||||
obj.increment('items.1.count', 4);
|
||||
await obj.save();
|
||||
expect(obj.toJSON().items[0].value).toBe('a');
|
||||
expect(obj.toJSON().items[1].value).toBe('b');
|
||||
expect(obj.toJSON().items[0].count).toBe(20);
|
||||
expect(obj.toJSON().items[1].count).toBe(5);
|
||||
const query = new Parse.Query(TestObject);
|
||||
const result = await query.get(obj.id);
|
||||
expect(result.get('items')[0].value).toBe('a');
|
||||
expect(result.get('items')[1].value).toBe('b');
|
||||
expect(result.get('items')[0].count).toBe(20);
|
||||
expect(result.get('items')[1].count).toBe(5);
|
||||
expect(result.get('items')).toEqual(obj.get('items'));
|
||||
});
|
||||
|
||||
it_only_db('mongo')('can increment array nested fields missing index', async () => {
|
||||
const obj = new TestObject();
|
||||
obj.set('items', []);
|
||||
await obj.save();
|
||||
obj.increment('items.1.count', 15);
|
||||
await obj.save();
|
||||
expect(obj.toJSON().items[0]).toBe(null);
|
||||
expect(obj.toJSON().items[1].count).toBe(15);
|
||||
const query = new Parse.Query(TestObject);
|
||||
const result = await query.get(obj.id);
|
||||
expect(result.get('items')[0]).toBe(null);
|
||||
expect(result.get('items')[1].count).toBe(15);
|
||||
expect(result.get('items')).toEqual(obj.get('items'));
|
||||
});
|
||||
|
||||
it('can query array nested fields', async () => {
|
||||
const objects = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const obj = new TestObject();
|
||||
obj.set('items', [i, { value: i }]);
|
||||
objects.push(obj);
|
||||
}
|
||||
await Parse.Object.saveAll(objects);
|
||||
let query = new Parse.Query(TestObject);
|
||||
query.greaterThan('items.1.value', 5);
|
||||
let result = await query.find();
|
||||
expect(result.length).toBe(4);
|
||||
|
||||
query = new Parse.Query(TestObject);
|
||||
query.lessThan('items.0', 3);
|
||||
result = await query.find();
|
||||
expect(result.length).toBe(3);
|
||||
|
||||
query = new Parse.Query(TestObject);
|
||||
query.equalTo('items.0', 5);
|
||||
result = await query.find();
|
||||
expect(result.length).toBe(1);
|
||||
|
||||
query = new Parse.Query(TestObject);
|
||||
query.notEqualTo('items.0', 5);
|
||||
result = await query.find();
|
||||
expect(result.length).toBe(9);
|
||||
});
|
||||
|
||||
it('addUnique with object', function (done) {
|
||||
const x1 = new Parse.Object('X');
|
||||
x1.set('stuff', [1, { hello: 'world' }, { foo: 'bar' }]);
|
||||
@@ -2119,56 +2055,4 @@ describe('Parse.Object testing', () => {
|
||||
const object = new Parse.Object('CloudCodeIsNew');
|
||||
await object.save();
|
||||
});
|
||||
|
||||
it('should not change the json field to array in afterSave', async () => {
|
||||
Parse.Cloud.beforeSave('failingJSONTestCase', req => {
|
||||
expect(req.object.get('jsonField')).toEqual({ '123': 'test' });
|
||||
});
|
||||
|
||||
Parse.Cloud.afterSave('failingJSONTestCase', req => {
|
||||
expect(req.object.get('jsonField')).toEqual({ '123': 'test' });
|
||||
});
|
||||
|
||||
const object = new Parse.Object('failingJSONTestCase');
|
||||
object.set('jsonField', { '123': 'test' });
|
||||
await object.save();
|
||||
});
|
||||
|
||||
it('returns correct field values', async () => {
|
||||
const values = [
|
||||
{ field: 'string', value: 'string' },
|
||||
{ field: 'number', value: 1 },
|
||||
{ field: 'boolean', value: true },
|
||||
{ field: 'array', value: [0, 1, 2] },
|
||||
{ field: 'array', value: [1, 2, 3] },
|
||||
{ field: 'array', value: [{ '0': 'a' }, 2, 3] },
|
||||
{ field: 'object', value: { key: 'value' } },
|
||||
{ field: 'object', value: { key1: 'value1', key2: 'value2' } },
|
||||
{ field: 'object', value: { key1: 1, key2: 2 } },
|
||||
{ field: 'object', value: { '1x1': 1 } },
|
||||
{ field: 'object', value: { '1x1': 1, '2': 2 } },
|
||||
{ field: 'object', value: { '0': 0 } },
|
||||
{ field: 'object', value: { '1': 1 } },
|
||||
{ field: 'object', value: { '0': { '0': 'a', '1': 'b' } } },
|
||||
{ field: 'date', value: new Date() },
|
||||
{
|
||||
field: 'file',
|
||||
value: Parse.File.fromJSON({
|
||||
__type: 'File',
|
||||
name: 'name',
|
||||
url: 'http://localhost:8378/1/files/test/name',
|
||||
}),
|
||||
},
|
||||
{ field: 'geoPoint', value: new Parse.GeoPoint(40, -30) },
|
||||
{ field: 'bytes', value: { __type: 'Bytes', base64: 'ZnJveW8=' } },
|
||||
];
|
||||
for (const value of values) {
|
||||
const object = new TestObject();
|
||||
object.set(value.field, value.value);
|
||||
await object.save();
|
||||
const query = new Parse.Query(TestObject);
|
||||
const objectAgain = await query.get(object.id);
|
||||
expect(objectAgain.get(value.field)).toEqual(value.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,28 +23,28 @@ const loadTestData = () => {
|
||||
const data1 = {
|
||||
score: 10,
|
||||
name: 'foo',
|
||||
sender: { group: 'A' },
|
||||
sender: { group: 'A' }, // TODO: change to `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
views: 900,
|
||||
size: ['S', 'M'],
|
||||
};
|
||||
const data2 = {
|
||||
score: 10,
|
||||
name: 'foo',
|
||||
sender: { group: 'A' },
|
||||
sender: { group: 'A' }, // TODO: change to `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
views: 800,
|
||||
size: ['M', 'L'],
|
||||
};
|
||||
const data3 = {
|
||||
score: 10,
|
||||
name: 'bar',
|
||||
sender: { group: 'B' },
|
||||
sender: { group: 'B' }, // TODO: change to `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
views: 700,
|
||||
size: ['S'],
|
||||
};
|
||||
const data4 = {
|
||||
score: 20,
|
||||
name: 'dpl',
|
||||
sender: { group: 'B' },
|
||||
sender: { group: 'B' }, // TODO: change to `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
views: 700,
|
||||
size: ['S'],
|
||||
};
|
||||
@@ -86,7 +86,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('invalid query group _id required', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$group: {},
|
||||
group: {}, // TODO: write as `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options).catch(error => {
|
||||
@@ -98,7 +98,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('group by field', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$group: { _id: '$name' },
|
||||
group: { objectId: '$name' }, // TODO: write as `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -119,7 +119,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
pipeline: {
|
||||
$group: { _id: '$name' },
|
||||
group: { objectId: '$name' }, // TODO: write as `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -137,7 +137,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj = new TestObject();
|
||||
const pipeline = [
|
||||
{
|
||||
$group: { _id: {} },
|
||||
group: { objectId: {} }, // TODO: write as `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
},
|
||||
];
|
||||
obj
|
||||
@@ -156,7 +156,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj = new TestObject();
|
||||
const pipeline = [
|
||||
{
|
||||
$group: { _id: '' },
|
||||
group: { objectId: '' }, // TODO: write as `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
},
|
||||
];
|
||||
obj
|
||||
@@ -175,7 +175,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj = new TestObject();
|
||||
const pipeline = [
|
||||
{
|
||||
$group: { _id: [] },
|
||||
group: { objectId: [] }, // TODO: write as `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
},
|
||||
];
|
||||
obj
|
||||
@@ -196,8 +196,9 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj3 = new TestObject();
|
||||
const pipeline = [
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: {
|
||||
objectId: {
|
||||
score: '$score',
|
||||
views: '$views',
|
||||
},
|
||||
@@ -279,7 +280,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('group by number', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$group: { _id: '$score' },
|
||||
group: { objectId: '$score' }, // TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -301,8 +302,9 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj2 = new TestObject({ name: 'item b', quantity: 5, price: 5 });
|
||||
const pipeline = [
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: {
|
||||
objectId: null,
|
||||
total: { $sum: { $multiply: ['$quantity', '$price'] } },
|
||||
},
|
||||
},
|
||||
@@ -324,10 +326,10 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj2 = new TestObject({ name: 'item b', quantity: 5, price: 5 });
|
||||
const pipeline = [
|
||||
{
|
||||
$match: { quantity: { $exists: true } },
|
||||
match: { quantity: { $exists: true } },
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
project: {
|
||||
name: 1,
|
||||
total: { $multiply: ['$quantity', '$price'] },
|
||||
},
|
||||
@@ -356,16 +358,16 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj2 = new TestObject({ name: 'item b', quantity: 5, price: 5 });
|
||||
const pipeline = [
|
||||
{
|
||||
$match: { quantity: { $exists: true } },
|
||||
match: { quantity: { $exists: true } },
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
project: {
|
||||
objectId: 0, // TODO: change to `_id`. See [#7339](https://bit.ly/3incnWx)
|
||||
total: { $multiply: ['$quantity', '$price'] },
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: { total: 1 },
|
||||
sort: { total: 1 },
|
||||
},
|
||||
];
|
||||
Parse.Object.saveAll([obj1, obj2])
|
||||
@@ -386,7 +388,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it_exclude_dbs(['postgres'])('project updatedAt only transform', done => {
|
||||
const pipeline = [
|
||||
{
|
||||
$project: { _id: 0, updatedAt: 1 },
|
||||
project: { objectId: 0, updatedAt: 1 },
|
||||
},
|
||||
];
|
||||
const query = new Parse.Query(TestObject);
|
||||
@@ -409,13 +411,13 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj3 = new TestObject({ dateField2019: new Date(1990, 11, 1) });
|
||||
const pipeline = [
|
||||
{
|
||||
$match: {
|
||||
match: {
|
||||
dateField2019: { $exists: true },
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
group: {
|
||||
objectId: {
|
||||
day: { $dayOfMonth: '$dateField2019' },
|
||||
month: { $month: '$dateField2019' },
|
||||
year: { $year: '$dateField2019' },
|
||||
@@ -447,8 +449,9 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj3 = new TestObject({ dateField2019: new Date(1990, 11, 1) });
|
||||
const pipeline = [
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: {
|
||||
objectId: {
|
||||
day: { $dayOfMonth: '$dateField2019' },
|
||||
month: { $month: '$dateField2019' },
|
||||
year: { $year: '$dateField2019' },
|
||||
@@ -478,7 +481,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj1 = new TestObject({ pointer: pointer1 });
|
||||
const obj2 = new TestObject({ pointer: pointer2 });
|
||||
const obj3 = new TestObject({ pointer: pointer1 });
|
||||
const pipeline = [{ $group: { _id: '$pointer' } }];
|
||||
const pipeline = [{ group: { objectId: '$pointer' } }];
|
||||
Parse.Object.saveAll([pointer1, pointer2, obj1, obj2, obj3])
|
||||
.then(() => {
|
||||
const query = new Parse.Query(TestObject);
|
||||
@@ -496,7 +499,8 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('group sum query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$group: { _id: null, total: { $sum: '$score' } },
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: { objectId: null, total: { $sum: '$score' } },
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -512,7 +516,8 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('group count query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$group: { _id: null, total: { $sum: 1 } },
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: { objectId: null, total: { $sum: 1 } },
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -528,7 +533,8 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('group min query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$group: { _id: null, minScore: { $min: '$score' } },
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: { objectId: null, minScore: { $min: '$score' } },
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -544,7 +550,8 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('group max query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$group: { _id: null, maxScore: { $max: '$score' } },
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: { objectId: null, maxScore: { $max: '$score' } },
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -560,7 +567,8 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('group avg query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$group: { _id: null, avgScore: { $avg: '$score' } },
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: { objectId: null, avgScore: { $avg: '$score' } },
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -576,7 +584,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('limit query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$limit: 2,
|
||||
limit: 2,
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -590,7 +598,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('sort ascending query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$sort: { name: 1 },
|
||||
sort: { name: 1 },
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -608,7 +616,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('sort decending query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$sort: { name: -1 },
|
||||
sort: { name: -1 },
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -626,7 +634,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('skip query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$skip: 2,
|
||||
skip: 2,
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -646,7 +654,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj1 = new TestObject({ dateField: yesterday });
|
||||
const obj2 = new TestObject({ dateField: today });
|
||||
const obj3 = new TestObject({ dateField: tomorrow });
|
||||
const pipeline = [{ $match: { dateField: { $lt: tomorrow } } }];
|
||||
const pipeline = [{ match: { dateField: { $lt: tomorrow } } }];
|
||||
Parse.Object.saveAll([obj1, obj2, obj3])
|
||||
.then(() => {
|
||||
const query = new Parse.Query(TestObject);
|
||||
@@ -678,7 +686,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('match comparison query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$match: { score: { $gt: 15 } },
|
||||
match: { score: { $gt: 15 } },
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -693,7 +701,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('match multiple comparison query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$match: { score: { $gt: 5, $lt: 15 } },
|
||||
match: { score: { $gt: 5, $lt: 15 } },
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -710,7 +718,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('match complex comparison query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$match: { score: { $gt: 5, $lt: 15 }, views: { $gt: 850, $lt: 1000 } },
|
||||
match: { score: { $gt: 5, $lt: 15 }, views: { $gt: 850, $lt: 1000 } },
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -726,7 +734,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('match comparison and equality query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$match: { score: { $gt: 5, $lt: 15 }, views: 900 },
|
||||
match: { score: { $gt: 5, $lt: 15 }, views: 900 },
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -742,7 +750,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('match $or query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$match: {
|
||||
match: {
|
||||
$or: [{ score: { $gt: 15, $lt: 25 } }, { views: { $gt: 750, $lt: 850 } }],
|
||||
},
|
||||
},
|
||||
@@ -767,7 +775,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj2 = new TestObject();
|
||||
Parse.Object.saveAll([obj1, obj2])
|
||||
.then(() => {
|
||||
const pipeline = [{ $match: { _id: obj1.id } }];
|
||||
const pipeline = [{ match: { objectId: obj1.id } }];
|
||||
const query = new Parse.Query(TestObject);
|
||||
return query.aggregate(pipeline);
|
||||
})
|
||||
@@ -783,7 +791,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj2 = new TestObject({ name: 'TestObject2' });
|
||||
Parse.Object.saveAll([obj1, obj2])
|
||||
.then(() => {
|
||||
const pipeline = [{ $match: { name: 'TestObject1' } }];
|
||||
const pipeline = [{ match: { name: 'TestObject1' } }];
|
||||
const query = new Parse.Query(TestObject);
|
||||
return query.aggregate(pipeline);
|
||||
})
|
||||
@@ -803,7 +811,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
|
||||
Parse.Object.saveAll([pointer1, pointer2, obj1, obj2, obj3])
|
||||
.then(() => {
|
||||
const pipeline = [{ $match: { pointer: pointer1.id } }];
|
||||
const pipeline = [{ match: { pointer: pointer1.id } }];
|
||||
const query = new Parse.Query(TestObject);
|
||||
return query.aggregate(pipeline);
|
||||
})
|
||||
@@ -818,7 +826,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('match exists query', done => {
|
||||
const pipeline = [{ $match: { score: { $exists: true } } }];
|
||||
const pipeline = [{ match: { score: { $exists: true } } }];
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.aggregate(pipeline).then(results => {
|
||||
expect(results.length).toEqual(4);
|
||||
@@ -834,7 +842,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
.then(() => {
|
||||
const now = new Date();
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const pipeline = [{ $match: { createdAt: { $gte: today } } }];
|
||||
const pipeline = [{ match: { createdAt: { $gte: today } } }];
|
||||
const query = new Parse.Query(TestObject);
|
||||
return query.aggregate(pipeline);
|
||||
})
|
||||
@@ -853,7 +861,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
.then(() => {
|
||||
const now = new Date();
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const pipeline = [{ $match: { updatedAt: { $gte: today } } }];
|
||||
const pipeline = [{ match: { updatedAt: { $gte: today } } }];
|
||||
const query = new Parse.Query(TestObject);
|
||||
return query.aggregate(pipeline);
|
||||
})
|
||||
@@ -872,7 +880,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
.then(() => {
|
||||
const now = new Date();
|
||||
const future = new Date(now.getFullYear(), now.getMonth() + 1, now.getDate());
|
||||
const pipeline = [{ $match: { createdAt: future } }];
|
||||
const pipeline = [{ match: { createdAt: future } }];
|
||||
const query = new Parse.Query(TestObject);
|
||||
return query.aggregate(pipeline);
|
||||
})
|
||||
@@ -891,7 +899,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
|
||||
Parse.Object.saveAll([pointer, obj1, obj2, obj3])
|
||||
.then(() => {
|
||||
const pipeline = [{ $match: { pointer: { $exists: true } } }];
|
||||
const pipeline = [{ match: { pointer: { $exists: true } } }];
|
||||
const query = new Parse.Query(TestObject);
|
||||
return query.aggregate(pipeline);
|
||||
})
|
||||
@@ -930,7 +938,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
(
|
||||
await new Parse.Query('MyCollection').aggregate([
|
||||
{
|
||||
$match: {
|
||||
match: {
|
||||
language: { $in: [null, 'en'] },
|
||||
},
|
||||
},
|
||||
@@ -944,7 +952,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
(
|
||||
await new Parse.Query('MyCollection').aggregate([
|
||||
{
|
||||
$match: {
|
||||
match: {
|
||||
$or: [{ language: 'en' }, { language: null }],
|
||||
},
|
||||
},
|
||||
@@ -958,7 +966,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('project query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$project: { name: 1 },
|
||||
project: { name: 1 },
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -978,7 +986,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('multiple project query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$project: { name: 1, score: 1, sender: 1 },
|
||||
project: { name: 1, score: 1, sender: 1 },
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -1003,8 +1011,8 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
.save()
|
||||
.then(() => {
|
||||
const pipeline = [
|
||||
{ $match: { _id: obj.id } },
|
||||
{ $project: { pointer: 1, name: 1, createdAt: 1 } },
|
||||
{ match: { objectId: obj.id } },
|
||||
{ project: { pointer: 1, name: 1, createdAt: 1 } },
|
||||
];
|
||||
const query = new Parse.Query(TestObject);
|
||||
return query.aggregate(pipeline);
|
||||
@@ -1021,8 +1029,9 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('project with group query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$project: { score: 1 },
|
||||
$group: { _id: '$score', score: { $sum: '$score' } },
|
||||
project: { score: 1 },
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: { objectId: '$score', score: { $sum: '$score' } },
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -1049,7 +1058,8 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('class does not exist return empty', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$group: { _id: null, total: { $sum: '$score' } },
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: { objectId: null, total: { $sum: '$score' } },
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/UnknownClass', options)
|
||||
@@ -1063,7 +1073,8 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('field does not exist return empty', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$group: { _id: null, total: { $sum: '$unknownfield' } },
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: { objectId: null, total: { $sum: '$unknownfield' } },
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/UnknownClass', options)
|
||||
@@ -1092,7 +1103,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
distinct: 'score',
|
||||
$where: {
|
||||
where: {
|
||||
name: 'bar',
|
||||
},
|
||||
},
|
||||
@@ -1109,7 +1120,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
distinct: 'score',
|
||||
$where: JSON.stringify({ name: 'bar' }),
|
||||
where: JSON.stringify({ name: 'bar' }),
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -1259,7 +1270,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('does not return sensitive hidden properties', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
$match: {
|
||||
match: {
|
||||
score: {
|
||||
$gt: 5,
|
||||
},
|
||||
@@ -1306,7 +1317,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('aggregate allow multiple of same stage', async done => {
|
||||
await reconfigureServer({ silent: false });
|
||||
await reconfigureServer();
|
||||
const pointer1 = new TestObject({ value: 1 });
|
||||
const pointer2 = new TestObject({ value: 2 });
|
||||
const pointer3 = new TestObject({ value: 3 });
|
||||
@@ -1319,17 +1330,17 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
body: {
|
||||
pipeline: [
|
||||
{
|
||||
$match: { name: 'Hello' },
|
||||
match: { name: 'Hello' },
|
||||
},
|
||||
{
|
||||
// Transform className$objectId to objectId and store in new field tempPointer
|
||||
$project: {
|
||||
project: {
|
||||
tempPointer: { $substr: ['$_p_pointer', 11, -1] }, // Remove TestObject$
|
||||
},
|
||||
},
|
||||
{
|
||||
// Left Join, replace objectId stored in tempPointer with an actual object
|
||||
$lookup: {
|
||||
lookup: {
|
||||
from: 'test_TestObject',
|
||||
localField: 'tempPointer',
|
||||
foreignField: '_id',
|
||||
@@ -1338,12 +1349,12 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
},
|
||||
{
|
||||
// lookup returns an array, Deconstructs an array field to objects
|
||||
$unwind: {
|
||||
unwind: {
|
||||
path: '$tempPointer',
|
||||
},
|
||||
},
|
||||
{
|
||||
$match: { 'tempPointer.value': 2 },
|
||||
match: { 'tempPointer.value': 2 },
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1387,7 +1398,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
// Create query
|
||||
const pipeline = [
|
||||
{
|
||||
$geoNear: {
|
||||
geoNear: {
|
||||
near: {
|
||||
type: 'Point',
|
||||
coordinates: [1, 1],
|
||||
@@ -1440,7 +1451,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
// Create query
|
||||
const pipeline = [
|
||||
{
|
||||
$geoNear: {
|
||||
geoNear: {
|
||||
near: {
|
||||
type: 'Point',
|
||||
coordinates: [1, 1],
|
||||
@@ -1486,7 +1497,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
// Create query
|
||||
const pipeline = [
|
||||
{
|
||||
$geoNear: {
|
||||
geoNear: {
|
||||
near: [1, 1],
|
||||
key: 'location',
|
||||
spherical: true,
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const Config = require('../lib/Config');
|
||||
const { MongoClient } = require('mongodb');
|
||||
const databaseURI = 'mongodb://localhost:27017/';
|
||||
const request = require('../lib/request');
|
||||
|
||||
let config, client, database;
|
||||
|
||||
const masterKeyHeaders = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Rest-API-Key': 'rest',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
const masterKeyOptions = {
|
||||
headers: masterKeyHeaders,
|
||||
json: true,
|
||||
};
|
||||
|
||||
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,
|
||||
});
|
||||
database = client.db('parseServerMongoAdapterTestDatabase');
|
||||
let profiler = await database.command({ profile: 0 });
|
||||
expect(profiler.was).toEqual(0);
|
||||
// console.log(`Disabling profiler : ${profiler.was}`);
|
||||
profiler = await database.command({ profile: profileLevel });
|
||||
profiler = await database.command({ profile: -1 });
|
||||
// console.log(`Enabling profiler : ${profiler.was}`);
|
||||
profiler = await database.command({ profile: -1 });
|
||||
expect(profiler.was).toEqual(profileLevel);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
const profiler = await database.command({ profile: -1 });
|
||||
expect(profiler.was).toEqual(profileLevel);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await database.command({ profile: 0 });
|
||||
await client.close();
|
||||
});
|
||||
|
||||
it('send comment with query through REST', async () => {
|
||||
const comment = 'Hello Parse';
|
||||
const object = new TestObject();
|
||||
object.set('name', 'object');
|
||||
await object.save();
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
url: Parse.serverURL + '/classes/TestObject',
|
||||
qs: {
|
||||
explain: true,
|
||||
comment: comment,
|
||||
},
|
||||
});
|
||||
await request(options);
|
||||
const result = await database.collection('system.profile').findOne({}, { sort: { ts: -1 } });
|
||||
expect(result.command.explain.comment).toBe(comment);
|
||||
});
|
||||
|
||||
it('send comment with query', async () => {
|
||||
const comment = 'Hello Parse';
|
||||
const object = new TestObject();
|
||||
object.set('name', 'object');
|
||||
await object.save();
|
||||
const collection = await config.database.adapter._adaptiveCollection('TestObject');
|
||||
await collection._rawFind({ name: 'object' }, { comment: comment });
|
||||
const result = await database.collection('system.profile').findOne({}, { sort: { ts: -1 } });
|
||||
expect(result.command.comment).toBe(comment);
|
||||
});
|
||||
|
||||
it('send a comment with a count query', async () => {
|
||||
const comment = 'Hello Parse';
|
||||
const object = new TestObject();
|
||||
object.set('name', 'object');
|
||||
await object.save();
|
||||
|
||||
const object2 = new TestObject();
|
||||
object2.set('name', 'object');
|
||||
await object2.save();
|
||||
|
||||
const collection = await config.database.adapter._adaptiveCollection('TestObject');
|
||||
const countResult = await collection.count({ name: 'object' }, { comment: comment });
|
||||
expect(countResult).toEqual(2);
|
||||
const result = await database.collection('system.profile').findOne({}, { sort: { ts: -1 } });
|
||||
expect(result.command.comment).toBe(comment);
|
||||
});
|
||||
|
||||
it('attach a comment to an aggregation', async () => {
|
||||
const comment = 'Hello Parse';
|
||||
const object = new TestObject();
|
||||
object.set('name', 'object');
|
||||
await object.save();
|
||||
const collection = await config.database.adapter._adaptiveCollection('TestObject');
|
||||
await collection.aggregate([{ $group: { _id: '$name' } }], {
|
||||
explain: true,
|
||||
comment: comment,
|
||||
});
|
||||
const result = await database.collection('system.profile').findOne({}, { sort: { ts: -1 } });
|
||||
expect(result.command.explain.comment).toBe(comment);
|
||||
});
|
||||
});
|
||||
@@ -86,7 +86,6 @@ 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');
|
||||
|
||||
@@ -94,7 +93,6 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
hint: '_id_',
|
||||
explain: true,
|
||||
});
|
||||
|
||||
queryPlanner = result[0].stages[0].$cursor.queryPlanner;
|
||||
expect(queryPlanner.winningPlan.stage).toBe('FETCH');
|
||||
expect(queryPlanner.winningPlan.inputStage.indexName).toBe('_id_');
|
||||
@@ -324,7 +322,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
url: Parse.serverURL + '/aggregate/TestObject',
|
||||
qs: {
|
||||
explain: true,
|
||||
$group: JSON.stringify({ _id: '$foo' }),
|
||||
group: JSON.stringify({ objectId: '$foo' }),
|
||||
},
|
||||
});
|
||||
let response = await request(options);
|
||||
@@ -336,7 +334,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
qs: {
|
||||
explain: true,
|
||||
hint: '_id_',
|
||||
$group: JSON.stringify({ _id: '$foo' }),
|
||||
group: JSON.stringify({ objectId: '$foo' }),
|
||||
},
|
||||
});
|
||||
response = await request(options);
|
||||
@@ -351,7 +349,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
url: Parse.serverURL + '/aggregate/TestObject',
|
||||
qs: {
|
||||
explain: true,
|
||||
$group: JSON.stringify({ _id: '$foo' }),
|
||||
group: JSON.stringify({ objectId: '$foo' }),
|
||||
},
|
||||
});
|
||||
let response = await request(options);
|
||||
@@ -365,7 +363,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
qs: {
|
||||
explain: true,
|
||||
hint: '_id_',
|
||||
$group: JSON.stringify({ _id: '$foo' }),
|
||||
group: JSON.stringify({ objectId: '$foo' }),
|
||||
},
|
||||
});
|
||||
response = await request(options);
|
||||
@@ -384,7 +382,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
url: Parse.serverURL + '/aggregate/TestObject',
|
||||
qs: {
|
||||
explain: true,
|
||||
$group: JSON.stringify({ _id: '$foo' }),
|
||||
group: JSON.stringify({ objectId: '$foo' }),
|
||||
},
|
||||
});
|
||||
let response = await request(options);
|
||||
@@ -398,7 +396,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
qs: {
|
||||
explain: true,
|
||||
hint: '_id_',
|
||||
$group: JSON.stringify({ _id: '$foo' }),
|
||||
group: JSON.stringify({ objectId: '$foo' }),
|
||||
},
|
||||
});
|
||||
response = await request(options);
|
||||
@@ -417,7 +415,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
url: Parse.serverURL + '/aggregate/TestObject',
|
||||
qs: {
|
||||
explain: true,
|
||||
$group: JSON.stringify({ _id: '$foo' }),
|
||||
group: JSON.stringify({ objectId: '$foo' }),
|
||||
},
|
||||
});
|
||||
let response = await request(options);
|
||||
@@ -431,7 +429,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
qs: {
|
||||
explain: true,
|
||||
hint: '_id_',
|
||||
$group: JSON.stringify({ _id: '$foo' }),
|
||||
group: JSON.stringify({ objectId: '$foo' }),
|
||||
},
|
||||
});
|
||||
response = await request(options);
|
||||
|
||||
@@ -142,7 +142,7 @@ describe('Parse Role testing', () => {
|
||||
return Promise.all(promises);
|
||||
};
|
||||
|
||||
const restExecute = spyOn(RestQuery._UnsafeRestQuery.prototype, 'execute').and.callThrough();
|
||||
const restExecute = spyOn(RestQuery.prototype, 'execute').and.callThrough();
|
||||
|
||||
let user, auth, getAllRolesSpy;
|
||||
createTestUser()
|
||||
|
||||
+17
-10
@@ -29,20 +29,27 @@ describe('Server Url Checks', () => {
|
||||
server.close(done);
|
||||
});
|
||||
|
||||
it('validate good server url', async () => {
|
||||
it('validate good server url', done => {
|
||||
Parse.serverURL = 'http://localhost:13376';
|
||||
const response = await ParseServer.verifyServerUrl();
|
||||
expect(response).toBeTrue();
|
||||
ParseServer.verifyServerUrl(async result => {
|
||||
if (!result) {
|
||||
done.fail('Did not pass valid url');
|
||||
}
|
||||
await reconfigureServer();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('mark bad server url', async () => {
|
||||
it('mark bad server url', done => {
|
||||
spyOn(console, 'warn').and.callFake(() => {});
|
||||
Parse.serverURL = 'notavalidurl';
|
||||
const response = await ParseServer.verifyServerUrl();
|
||||
expect(response).not.toBeTrue();
|
||||
expect(console.warn).toHaveBeenCalledWith(
|
||||
`\nWARNING, Unable to connect to 'notavalidurl' as the URL is invalid. Cloud code and push notifications may be unavailable!\n`
|
||||
);
|
||||
ParseServer.verifyServerUrl(async result => {
|
||||
if (result) {
|
||||
done.fail('Did not mark invalid url');
|
||||
}
|
||||
await reconfigureServer();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
xit('handleShutdown, close connection', done => {
|
||||
@@ -85,7 +92,7 @@ describe('Server Url Checks', () => {
|
||||
close = true;
|
||||
},
|
||||
});
|
||||
const parseServer = ParseServer.startApp(newConfiguration);
|
||||
const parseServer = ParseServer.start(newConfiguration);
|
||||
});
|
||||
|
||||
it('does not have unhandled promise rejection in the case of load error', done => {
|
||||
|
||||
@@ -2,6 +2,7 @@ const ParseServerRESTController = require('../lib/ParseServerRESTController')
|
||||
.ParseServerRESTController;
|
||||
const ParseServer = require('../lib/ParseServer').default;
|
||||
const Parse = require('parse/node').Parse;
|
||||
const semver = require('semver');
|
||||
const TestUtils = require('../lib/TestUtils');
|
||||
|
||||
let RESTController;
|
||||
@@ -129,13 +130,19 @@ describe('ParseServerRESTController', () => {
|
||||
});
|
||||
|
||||
if (
|
||||
process.env.MONGODB_TOPOLOGY === 'replicaset' ||
|
||||
(semver.satisfies(process.env.MONGODB_VERSION, '>=4.0.4') &&
|
||||
process.env.MONGODB_TOPOLOGY === 'replicaset' &&
|
||||
process.env.MONGODB_STORAGE_ENGINE === 'wiredTiger') ||
|
||||
process.env.PARSE_SERVER_TEST_DB === 'postgres'
|
||||
) {
|
||||
describe('transactions', () => {
|
||||
beforeEach(async () => {
|
||||
await TestUtils.destroyAllDataPermanently(true);
|
||||
if (process.env.MONGODB_TOPOLOGY === 'replicaset') {
|
||||
if (
|
||||
semver.satisfies(process.env.MONGODB_VERSION, '>=4.0.4') &&
|
||||
process.env.MONGODB_TOPOLOGY === 'replicaset' &&
|
||||
process.env.MONGODB_STORAGE_ENGINE === 'wiredTiger'
|
||||
) {
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI:
|
||||
@@ -631,58 +638,4 @@ describe('ParseServerRESTController', () => {
|
||||
expect(sessions[0].get('installationId')).toBe(installationId);
|
||||
expect(sessions[0].get('sessionToken')).toBe(loggedUser.sessionToken);
|
||||
});
|
||||
|
||||
it('returns a statusId when running jobs', async () => {
|
||||
Parse.Cloud.job('CloudJob', () => {
|
||||
return 'Cloud job completed';
|
||||
});
|
||||
const res = await RESTController.request(
|
||||
'POST',
|
||||
'/jobs/CloudJob',
|
||||
{},
|
||||
{ useMasterKey: true, returnStatus: true }
|
||||
);
|
||||
const jobStatusId = res._headers['X-Parse-Job-Status-Id'];
|
||||
expect(jobStatusId).toBeDefined();
|
||||
const result = await Parse.Cloud.getJobStatus(jobStatusId);
|
||||
expect(result.id).toBe(jobStatusId);
|
||||
});
|
||||
|
||||
it('returns a statusId when running push notifications', async () => {
|
||||
const payload = {
|
||||
data: { alert: 'We return status!' },
|
||||
where: { deviceType: 'ios' },
|
||||
};
|
||||
const res = await RESTController.request('POST', '/push', payload, {
|
||||
useMasterKey: true,
|
||||
returnStatus: true,
|
||||
});
|
||||
const pushStatusId = res._headers['X-Parse-Push-Status-Id'];
|
||||
expect(pushStatusId).toBeDefined();
|
||||
|
||||
const result = await Parse.Push.getPushStatus(pushStatusId);
|
||||
expect(result.id).toBe(pushStatusId);
|
||||
});
|
||||
|
||||
it('returns a statusId when running batch push notifications', async () => {
|
||||
const payload = {
|
||||
data: { alert: 'We return status!' },
|
||||
where: { deviceType: 'ios' },
|
||||
};
|
||||
const res = await RESTController.request('POST', 'batch', {
|
||||
requests: [{
|
||||
method: 'POST',
|
||||
path: '/push',
|
||||
body: payload,
|
||||
}],
|
||||
}, {
|
||||
useMasterKey: true,
|
||||
returnStatus: true,
|
||||
});
|
||||
const pushStatusId = res[0]._headers['X-Parse-Push-Status-Id'];
|
||||
expect(pushStatusId).toBeDefined();
|
||||
|
||||
const result = await Parse.Push.getPushStatus(pushStatusId);
|
||||
expect(result.id).toBe(pushStatusId);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
const request = require('../lib/request');
|
||||
|
||||
function setupTestUsers() {
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
const user1 = new Parse.User();
|
||||
const user2 = new Parse.User();
|
||||
const user3 = new Parse.User();
|
||||
@@ -20,10 +18,6 @@ function setupTestUsers() {
|
||||
user2.set('password', 'password');
|
||||
user3.set('password', 'password');
|
||||
|
||||
user1.setACL(acl);
|
||||
user2.setACL(acl);
|
||||
user3.setACL(acl);
|
||||
|
||||
return user1
|
||||
.signUp()
|
||||
.then(() => {
|
||||
|
||||
+76
-269
@@ -13,30 +13,6 @@ const passwordCrypto = require('../lib/password');
|
||||
const Config = require('../lib/Config');
|
||||
const cryptoUtils = require('../lib/cryptoUtils');
|
||||
|
||||
describe('allowExpiredAuthDataToken option', () => {
|
||||
it('should accept true value', async () => {
|
||||
await reconfigureServer({ allowExpiredAuthDataToken: true });
|
||||
expect(Config.get(Parse.applicationId).allowExpiredAuthDataToken).toBe(true);
|
||||
});
|
||||
|
||||
it('should accept false value', async () => {
|
||||
await reconfigureServer({ allowExpiredAuthDataToken: false });
|
||||
expect(Config.get(Parse.applicationId).allowExpiredAuthDataToken).toBe(false);
|
||||
});
|
||||
|
||||
it('should default false', async () => {
|
||||
await reconfigureServer({});
|
||||
expect(Config.get(Parse.applicationId).allowExpiredAuthDataToken).toBe(false);
|
||||
});
|
||||
|
||||
it('should enforce boolean values', async () => {
|
||||
const options = [[], 'a', '', 0, 1, {}, 'true', 'false'];
|
||||
for (const option of options) {
|
||||
await expectAsync(reconfigureServer({ allowExpiredAuthDataToken: option })).toBeRejected();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Parse.User testing', () => {
|
||||
it('user sign up class method', async done => {
|
||||
const user = await Parse.User.signUp('asdf', 'zxcv');
|
||||
@@ -74,36 +50,6 @@ describe('Parse.User testing', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('user login with context', async () => {
|
||||
let hit = 0;
|
||||
const context = { foo: 'bar' };
|
||||
Parse.Cloud.beforeLogin(req => {
|
||||
expect(req.context).toEqual(context);
|
||||
hit++;
|
||||
});
|
||||
Parse.Cloud.afterLogin(req => {
|
||||
expect(req.context).toEqual(context);
|
||||
hit++;
|
||||
});
|
||||
await Parse.User.signUp('asdf', 'zxcv');
|
||||
await request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/login',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Cloud-Context': JSON.stringify(context),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: {
|
||||
_method: 'GET',
|
||||
username: 'asdf',
|
||||
password: 'zxcv',
|
||||
},
|
||||
});
|
||||
expect(hit).toBe(2);
|
||||
});
|
||||
|
||||
it('user login with non-string username with REST API', async done => {
|
||||
await Parse.User.signUp('asdf', 'zxcv');
|
||||
request({
|
||||
@@ -193,13 +139,14 @@ describe('Parse.User testing', () => {
|
||||
const ACL = user.getACL();
|
||||
expect(ACL.getReadAccess(user)).toBe(true);
|
||||
expect(ACL.getWriteAccess(user)).toBe(true);
|
||||
expect(ACL.getPublicReadAccess()).toBe(false);
|
||||
expect(ACL.getPublicReadAccess()).toBe(true);
|
||||
expect(ACL.getPublicWriteAccess()).toBe(false);
|
||||
const perms = ACL.permissionsById;
|
||||
expect(Object.keys(perms).length).toBe(1);
|
||||
expect(Object.keys(perms).length).toBe(2);
|
||||
expect(perms[user.id].read).toBe(true);
|
||||
expect(perms[user.id].write).toBe(true);
|
||||
expect(perms['*']).toBeUndefined();
|
||||
expect(perms['*'].read).toBe(true);
|
||||
expect(perms['*'].write).not.toBe(true);
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -871,8 +818,8 @@ describe('Parse.User testing', () => {
|
||||
kevin.set('password', 'mypass');
|
||||
await kevin.signUp();
|
||||
const query = new Parse.Query(Parse.User);
|
||||
const count = await query.find({ useMasterKey: true });
|
||||
equal(count.length, 2);
|
||||
const count = await query.count();
|
||||
equal(count, 2);
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -1182,7 +1129,7 @@ describe('Parse.User testing', () => {
|
||||
this.synchronizedExpiration = authData.expiration_date;
|
||||
return true;
|
||||
},
|
||||
getAuthType() {
|
||||
getAuthType: function () {
|
||||
return 'facebook';
|
||||
},
|
||||
deauthenticate: function () {
|
||||
@@ -1211,7 +1158,7 @@ describe('Parse.User testing', () => {
|
||||
synchronizedAuthToken: null,
|
||||
synchronizedExpiration: null,
|
||||
|
||||
authenticate(options) {
|
||||
authenticate: function (options) {
|
||||
if (this.shouldError) {
|
||||
options.error(this, 'An error occurred');
|
||||
} else if (this.shouldCancel) {
|
||||
@@ -1220,7 +1167,7 @@ describe('Parse.User testing', () => {
|
||||
options.success(this, this.authData);
|
||||
}
|
||||
},
|
||||
restoreAuthentication(authData) {
|
||||
restoreAuthentication: function (authData) {
|
||||
if (!authData) {
|
||||
this.synchronizedUserId = null;
|
||||
this.synchronizedAuthToken = null;
|
||||
@@ -1232,10 +1179,10 @@ describe('Parse.User testing', () => {
|
||||
this.synchronizedExpiration = authData.expiration_date;
|
||||
return true;
|
||||
},
|
||||
getAuthType() {
|
||||
getAuthType: function () {
|
||||
return 'myoauth';
|
||||
},
|
||||
deauthenticate() {
|
||||
deauthenticate: function () {
|
||||
this.loggedOut = true;
|
||||
this.restoreAuthentication(null);
|
||||
},
|
||||
@@ -1845,7 +1792,7 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should not allow login with expired authData token since allowExpiredAuthDataToken is set to false by default', async () => {
|
||||
it('should allow login with old authData token', done => {
|
||||
const provider = {
|
||||
authData: {
|
||||
id: '12345',
|
||||
@@ -1866,12 +1813,22 @@ describe('Parse.User testing', () => {
|
||||
};
|
||||
defaultConfiguration.auth.shortLivedAuth.setValidAccessToken('token');
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
await Parse.User._logInWith('shortLivedAuth', {});
|
||||
// Simulate a remotely expired token (like a short lived one)
|
||||
// In this case, we want success as it was valid once.
|
||||
// If the client needs an updated token, do lock the user out
|
||||
defaultConfiguration.auth.shortLivedAuth.setValidAccessToken('otherToken');
|
||||
await expectAsync(Parse.User._logInWith('shortLivedAuth', {})).toBeRejected();
|
||||
Parse.User._logInWith('shortLivedAuth', {})
|
||||
.then(() => {
|
||||
// Simulate a remotely expired token (like a short lived one)
|
||||
// In this case, we want success as it was valid once.
|
||||
// If the client needs an updated one, do lock the user out
|
||||
defaultConfiguration.auth.shortLivedAuth.setValidAccessToken('otherToken');
|
||||
return Parse.User._logInWith('shortLivedAuth', {});
|
||||
})
|
||||
.then(
|
||||
() => {
|
||||
done();
|
||||
},
|
||||
err => {
|
||||
done.fail(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow PUT request with stale auth Data', done => {
|
||||
@@ -2119,15 +2076,7 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
|
||||
it("querying for users doesn't get session tokens", done => {
|
||||
const user = new Parse.User();
|
||||
user.set('username', 'finn');
|
||||
user.set('password', 'human');
|
||||
user.set('foo', 'bar');
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
user.setACL(acl);
|
||||
user
|
||||
.signUp()
|
||||
Parse.User.signUp('finn', 'human', { foo: 'bar' })
|
||||
.then(function () {
|
||||
return Parse.User.logOut();
|
||||
})
|
||||
@@ -2136,9 +2085,6 @@ describe('Parse.User testing', () => {
|
||||
user.set('username', 'jake');
|
||||
user.set('password', 'dog');
|
||||
user.set('foo', 'baz');
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
user.setACL(acl);
|
||||
return user.signUp();
|
||||
})
|
||||
.then(function () {
|
||||
@@ -2165,14 +2111,7 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
|
||||
it('querying for users only gets the expected fields', done => {
|
||||
const user = new Parse.User();
|
||||
user.setUsername('finn');
|
||||
user.setPassword('human');
|
||||
user.set('foo', 'bar');
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
user.setACL(acl);
|
||||
user.signUp().then(() => {
|
||||
Parse.User.signUp('finn', 'human', { foo: 'bar' }).then(() => {
|
||||
request({
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
@@ -2321,14 +2260,37 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
|
||||
describe('anonymous users', () => {
|
||||
beforeEach(() => {
|
||||
const insensitiveCollisions = [
|
||||
'abcdefghijklmnop',
|
||||
'Abcdefghijklmnop',
|
||||
'ABcdefghijklmnop',
|
||||
'ABCdefghijklmnop',
|
||||
'ABCDefghijklmnop',
|
||||
'ABCDEfghijklmnop',
|
||||
'ABCDEFghijklmnop',
|
||||
'ABCDEFGhijklmnop',
|
||||
'ABCDEFGHijklmnop',
|
||||
'ABCDEFGHIjklmnop',
|
||||
'ABCDEFGHIJklmnop',
|
||||
'ABCDEFGHIJKlmnop',
|
||||
'ABCDEFGHIJKLmnop',
|
||||
'ABCDEFGHIJKLMnop',
|
||||
'ABCDEFGHIJKLMnop',
|
||||
'ABCDEFGHIJKLMNop',
|
||||
'ABCDEFGHIJKLMNOp',
|
||||
'ABCDEFGHIJKLMNOP',
|
||||
];
|
||||
|
||||
// need a bunch of spare random strings per api request
|
||||
spyOn(cryptoUtils, 'randomString').and.returnValues(...insensitiveCollisions);
|
||||
});
|
||||
|
||||
it('should not fail on case insensitive matches', async () => {
|
||||
spyOn(cryptoUtils, 'randomString').and.returnValue('abcdefghijklmnop');
|
||||
const logIn = id => Parse.User.logInWith('anonymous', { authData: { id } });
|
||||
const user1 = await logIn('test1');
|
||||
const user1 = await Parse.AnonymousUtils.logIn();
|
||||
const username1 = user1.get('username');
|
||||
|
||||
cryptoUtils.randomString.and.returnValue('ABCDEFGHIJKLMNOp');
|
||||
const user2 = await logIn('test2');
|
||||
const user2 = await Parse.AnonymousUtils.logIn();
|
||||
const username2 = user2.get('username');
|
||||
|
||||
expect(username1).not.toBeUndefined();
|
||||
@@ -3004,7 +2966,6 @@ describe('Parse.User testing', () => {
|
||||
},
|
||||
});
|
||||
})
|
||||
.then(() => jasmine.timeout())
|
||||
.then(() => {
|
||||
expect(emailCalled).toBe(true);
|
||||
expect(emailOptions).not.toBeUndefined();
|
||||
@@ -3162,35 +3123,6 @@ describe('Parse.User testing', () => {
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('should return current session with expired expiration date', async () => {
|
||||
await Parse.User.signUp('buser', 'somepass', null);
|
||||
const response = await request({
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8378/1/classes/_Session',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
},
|
||||
});
|
||||
const body = response.data;
|
||||
const id = body.results[0].objectId;
|
||||
const expiresAt = new Date(new Date().setYear(2015));
|
||||
await request({
|
||||
method: 'PUT',
|
||||
url: 'http://localhost:8378/1/classes/_Session/' + id,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: {
|
||||
expiresAt: { __type: 'Date', iso: expiresAt.toISOString() },
|
||||
},
|
||||
});
|
||||
const session = await Parse.Session.current();
|
||||
expect(session.get('expiresAt')).toEqual(expiresAt);
|
||||
});
|
||||
|
||||
it('should not create extraneous session tokens', done => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
config.database
|
||||
@@ -3440,9 +3372,6 @@ describe('Parse.User testing', () => {
|
||||
password: 'world',
|
||||
email: 'test@email.com',
|
||||
});
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
user.setACL(acl);
|
||||
|
||||
reconfigureServer({
|
||||
appName: 'unused',
|
||||
@@ -3519,128 +3448,40 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should not allow updates to hidden fields', async () => {
|
||||
it('should not allow updates to hidden fields', done => {
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: () => {},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => Promise.resolve(),
|
||||
};
|
||||
|
||||
const user = new Parse.User();
|
||||
user.set({
|
||||
username: 'hello',
|
||||
password: 'world',
|
||||
email: 'test@email.com',
|
||||
});
|
||||
await reconfigureServer({
|
||||
appName: 'unused',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
});
|
||||
await user.signUp();
|
||||
user.set('_email_verify_token', 'bad', { ignoreValidation: true });
|
||||
await expectAsync(user.save()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'Invalid field name: _email_verify_token.')
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow updates to fields with maintenanceKey', async () => {
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: () => {},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => Promise.resolve(),
|
||||
};
|
||||
const user = new Parse.User();
|
||||
user.set({
|
||||
username: 'hello',
|
||||
password: 'world',
|
||||
email: 'test@example.com',
|
||||
});
|
||||
await reconfigureServer({
|
||||
reconfigureServer({
|
||||
appName: 'unused',
|
||||
maintenanceKey: 'test2',
|
||||
verifyUserEmails: true,
|
||||
emailVerifyTokenValidityDuration: 5,
|
||||
accountLockout: {
|
||||
duration: 1,
|
||||
threshold: 1,
|
||||
},
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
});
|
||||
await user.signUp();
|
||||
for (let i = 0; i < 2; i++) {
|
||||
try {
|
||||
await Parse.User.logIn(user.getEmail(), 'abc');
|
||||
} catch (e) {
|
||||
expect(e.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
|
||||
expect(
|
||||
e.message === 'Invalid username/password.' ||
|
||||
e.message ===
|
||||
'Your account is locked due to multiple failed login attempts. Please try again after 1 minute(s)'
|
||||
).toBeTrue();
|
||||
}
|
||||
}
|
||||
await Parse.User.requestPasswordReset(user.getEmail());
|
||||
const headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Rest-API-Key': 'rest',
|
||||
'X-Parse-Maintenance-Key': 'test2',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
const userMaster = await request({
|
||||
method: 'GET',
|
||||
url: `http://localhost:8378/1/classes/_User`,
|
||||
json: true,
|
||||
headers,
|
||||
}).then(res => res.data.results[0]);
|
||||
expect(Object.keys(userMaster).sort()).toEqual(
|
||||
[
|
||||
'ACL',
|
||||
'_account_lockout_expires_at',
|
||||
'_email_verify_token',
|
||||
'_email_verify_token_expires_at',
|
||||
'_failed_login_count',
|
||||
'_perishable_token',
|
||||
'createdAt',
|
||||
'email',
|
||||
'emailVerified',
|
||||
'objectId',
|
||||
'updatedAt',
|
||||
'username',
|
||||
].sort()
|
||||
);
|
||||
const toSet = {
|
||||
_account_lockout_expires_at: new Date(),
|
||||
_email_verify_token: 'abc',
|
||||
_email_verify_token_expires_at: new Date(),
|
||||
_failed_login_count: 0,
|
||||
_perishable_token_expires_at: new Date(),
|
||||
_perishable_token: 'abc',
|
||||
};
|
||||
await request({
|
||||
method: 'PUT',
|
||||
headers,
|
||||
url: Parse.serverURL + '/users/' + userMaster.objectId,
|
||||
json: true,
|
||||
body: toSet,
|
||||
}).then(res => res.data);
|
||||
const update = await request({
|
||||
method: 'GET',
|
||||
url: `http://localhost:8378/1/classes/_User`,
|
||||
json: true,
|
||||
headers,
|
||||
}).then(res => res.data.results[0]);
|
||||
for (const key in toSet) {
|
||||
const value = toSet[key];
|
||||
if (update[key] && update[key].iso) {
|
||||
expect(update[key].iso).toEqual(value.toISOString());
|
||||
} else if (value.toISOString) {
|
||||
expect(update[key]).toEqual(value.toISOString());
|
||||
} else {
|
||||
expect(update[key]).toEqual(value);
|
||||
}
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => {
|
||||
return Parse.User.current().set('_email_verify_token', 'bad').save();
|
||||
})
|
||||
.then(() => {
|
||||
fail('Should not be able to update email verification token');
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
expect(err).toBeDefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should revoke sessions when setting paswword with masterKey (#3289)', done => {
|
||||
@@ -4162,12 +4003,6 @@ describe('Parse.User testing', () => {
|
||||
silent: true,
|
||||
});
|
||||
|
||||
Parse.Cloud.beforeSave(Parse.User, ({ object }) => {
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
object.setACL(acl);
|
||||
});
|
||||
|
||||
const query = new Parse.Query(Parse.User);
|
||||
query.doesNotExist('foo');
|
||||
const subscription = await query.subscribe();
|
||||
@@ -4402,31 +4237,3 @@ describe('login as other user', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('allowClientClassCreation option', () => {
|
||||
it('should enforce boolean values', async () => {
|
||||
const options = [[], 'a', '', 0, 1, {}, 'true', 'false'];
|
||||
for (const option of options) {
|
||||
await expectAsync(reconfigureServer({ allowClientClassCreation: option })).toBeRejected();
|
||||
}
|
||||
});
|
||||
|
||||
it('should accept true value', async () => {
|
||||
await reconfigureServer({ allowClientClassCreation: true });
|
||||
expect(Config.get(Parse.applicationId).allowClientClassCreation).toBe(true);
|
||||
});
|
||||
|
||||
it('should accept false value', async () => {
|
||||
await reconfigureServer({ allowClientClassCreation: false });
|
||||
expect(Config.get(Parse.applicationId).allowClientClassCreation).toBe(false);
|
||||
});
|
||||
|
||||
it('should default false', async () => {
|
||||
// remove predefined allowClientClassCreation:true on global defaultConfiguration
|
||||
delete defaultConfiguration.allowClientClassCreation;
|
||||
await reconfigureServer(defaultConfiguration);
|
||||
expect(Config.get(Parse.applicationId).allowClientClassCreation).toBe(false);
|
||||
// Need to set it back to true to avoid other test fails
|
||||
defaultConfiguration.allowClientClassCreation = true;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1677,19 +1677,12 @@ describe('Password Policy: ', () => {
|
||||
});
|
||||
|
||||
it('should not infinitely loop if maxPasswordHistory is 1 (#4918)', async () => {
|
||||
const headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Rest-API-Key': 'test',
|
||||
'X-Parse-Maintenance-Key': 'test2',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
const user = new Parse.User();
|
||||
const query = new Parse.Query(Parse.User);
|
||||
|
||||
await reconfigureServer({
|
||||
appName: 'passwordPolicy',
|
||||
verifyUserEmails: false,
|
||||
maintenanceKey: 'test2',
|
||||
passwordPolicy: {
|
||||
maxPasswordHistory: 1,
|
||||
},
|
||||
@@ -1703,28 +1696,15 @@ describe('Password Policy: ', () => {
|
||||
user.setPassword('user2');
|
||||
await user.save();
|
||||
|
||||
const user1 = await query.get(user.id, { useMasterKey: true });
|
||||
expect(user1.get('_password_history')).toBeUndefined();
|
||||
|
||||
const result1 = await request({
|
||||
method: 'GET',
|
||||
url: `http://localhost:8378/1/classes/_User/${user.id}`,
|
||||
json: true,
|
||||
headers,
|
||||
}).then(res => res.data);
|
||||
expect(result1._password_history.length).toBe(1);
|
||||
const result1 = await query.get(user.id, { useMasterKey: true });
|
||||
expect(result1.get('_password_history').length).toBe(1);
|
||||
|
||||
user.setPassword('user3');
|
||||
await user.save();
|
||||
|
||||
const result2 = await request({
|
||||
method: 'GET',
|
||||
url: `http://localhost:8378/1/classes/_User/${user.id}`,
|
||||
json: true,
|
||||
headers,
|
||||
}).then(res => res.data);
|
||||
expect(result2._password_history.length).toBe(1);
|
||||
const result2 = await query.get(user.id, { useMasterKey: true });
|
||||
expect(result2.get('_password_history').length).toBe(1);
|
||||
|
||||
expect(result1._password_history).not.toEqual(result2._password_history);
|
||||
expect(result1.get('_password_history')).not.toEqual(result2.get('_password_history'));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,13 +27,13 @@ const baseURI = 'postgres://username:password@localhost:5432/db-name';
|
||||
const testfile = fs.readFileSync('./Dockerfile').toString();
|
||||
const dbOptionsTest = {};
|
||||
dbOptionsTest[
|
||||
`${baseURI}?ssl=true&binary=true&application_name=app_name&fallback_application_name=f_app_name&poolSize=12`
|
||||
`${baseURI}?ssl=true&binary=true&application_name=app_name&fallback_application_name=f_app_name&poolSize=10`
|
||||
] = {
|
||||
ssl: true,
|
||||
binary: true,
|
||||
application_name: 'app_name',
|
||||
fallback_application_name: 'f_app_name',
|
||||
max: 12,
|
||||
poolSize: 10,
|
||||
};
|
||||
dbOptionsTest[`${baseURI}?ssl=&binary=aa`] = {
|
||||
binary: false,
|
||||
@@ -83,20 +83,6 @@ describe('PostgresConfigParser.getDatabaseOptionsFromURI', () => {
|
||||
it('sets the poolSize to 10 if the it is not a number', () => {
|
||||
const result = parser.getDatabaseOptionsFromURI(`${baseURI}?poolSize=sdf`);
|
||||
|
||||
expect(result.max).toEqual(10);
|
||||
});
|
||||
|
||||
it('sets the max to 10 if the it is not a number', () => {
|
||||
const result = parser.getDatabaseOptionsFromURI(`${baseURI}?&max=sdf`);
|
||||
|
||||
expect(result.poolSize).toBeUndefined();
|
||||
expect(result.max).toEqual(10);
|
||||
});
|
||||
|
||||
it('max should take precedence over poolSize', () => {
|
||||
const result = parser.getDatabaseOptionsFromURI(`${baseURI}?poolSize=20&max=12`);
|
||||
|
||||
expect(result.poolSize).toBeUndefined();
|
||||
expect(result.max).toEqual(12);
|
||||
expect(result.poolSize).toEqual(10);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -362,7 +362,7 @@ describe_only_db('postgres')('PostgresStorageAdapter', () => {
|
||||
await dropTable(client, tableName);
|
||||
});
|
||||
|
||||
it('should use index for caseInsensitive query with user', async () => {
|
||||
it('should use index for caseInsensitive query', async () => {
|
||||
await adapter.deleteAllClasses();
|
||||
const config = Config.get('test');
|
||||
config.schemaCache.clear();
|
||||
|
||||
@@ -13,9 +13,6 @@ describe('ProtectedFields', function () {
|
||||
user.setPassword('sekrit');
|
||||
user.set('email', 'alice@aol.com');
|
||||
user.set('favoriteColor', 'yellow');
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
user.setACL(acl);
|
||||
await user.save();
|
||||
|
||||
const fetched = await new Parse.Query(Parse.User).get(user.id);
|
||||
@@ -38,9 +35,6 @@ describe('ProtectedFields', function () {
|
||||
user.set('timeZone', 'America/Los_Angeles');
|
||||
user.set('favoriteColor', 'yellow');
|
||||
user.set('favoriteFood', 'pizza');
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
user.setACL(acl);
|
||||
await user.save();
|
||||
|
||||
const fetched = await new Parse.Query(Parse.User).get(user.id);
|
||||
@@ -63,9 +57,6 @@ describe('ProtectedFields', function () {
|
||||
user.set('timeZone', 'America/Los_Angeles');
|
||||
user.set('favoriteColor', 'yellow');
|
||||
user.set('favoriteFood', 'pizza');
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
user.setACL(acl);
|
||||
await user.save();
|
||||
|
||||
const fetched = await new Parse.Query(Parse.User).get(user.id);
|
||||
@@ -117,9 +108,6 @@ describe('ProtectedFields', function () {
|
||||
user.set('timeZone', 'America/Los_Angeles');
|
||||
user.set('favoriteColor', 'yellow');
|
||||
user.set('favoriteFood', 'pizza');
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
user.setACL(acl);
|
||||
await user.save();
|
||||
|
||||
const objA = await new Parse.Object('ClassA').set('foo', 'zzz').set('bar', 'yyy').save();
|
||||
|
||||
@@ -26,12 +26,14 @@ const successfulIOS = function (body, installations) {
|
||||
return Promise.all(promises);
|
||||
};
|
||||
|
||||
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
const pushCompleted = async pushId => {
|
||||
const query = new Parse.Query('_PushStatus');
|
||||
query.equalTo('objectId', pushId);
|
||||
let result = await query.first({ useMasterKey: true });
|
||||
while (!(result && result.get('status') === 'succeeded')) {
|
||||
await jasmine.timeout();
|
||||
await sleep(100);
|
||||
result = await query.first({ useMasterKey: true });
|
||||
}
|
||||
};
|
||||
@@ -560,7 +562,7 @@ describe('PushController', () => {
|
||||
});
|
||||
const pushStatusId = await sendPush(payload, {}, config, auth);
|
||||
// it is enqueued so it can take time
|
||||
await jasmine.timeout(1000);
|
||||
await sleep(1000);
|
||||
Parse.serverURL = 'http://localhost:8378/1'; // GOOD url
|
||||
const result = await Parse.Push.getPushStatus(pushStatusId);
|
||||
expect(result).toBeDefined();
|
||||
@@ -799,7 +801,7 @@ describe('PushController', () => {
|
||||
});
|
||||
await Parse.Object.saveAll(installations);
|
||||
await pushController.sendPush(payload, {}, config, auth);
|
||||
await jasmine.timeout(1000);
|
||||
await sleep(1000);
|
||||
const query = new Parse.Query('_PushStatus');
|
||||
const results = await query.find({ useMasterKey: true });
|
||||
expect(results.length).toBe(1);
|
||||
@@ -854,7 +856,7 @@ describe('PushController', () => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
await Parse.Object.saveAll(installations);
|
||||
await pushController.sendPush(payload, {}, config, auth);
|
||||
await jasmine.timeout(1000);
|
||||
await sleep(1000);
|
||||
const query = new Parse.Query('_PushStatus');
|
||||
const results = await query.find({ useMasterKey: true });
|
||||
expect(results.length).toBe(1);
|
||||
|
||||
@@ -120,7 +120,7 @@ describe('PushWorker', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('transforms body appropriately with title locale', () => {
|
||||
it('transforms body appropriately', () => {
|
||||
const cleanBody = PushUtils.transformPushBodyForLocale(
|
||||
{
|
||||
data: {
|
||||
|
||||
@@ -125,35 +125,6 @@ describe('matchesQuery', function () {
|
||||
expect(matchesQuery(obj, q)).toBe(false);
|
||||
});
|
||||
|
||||
it('matches queries with eq constraint', function () {
|
||||
const obj = {
|
||||
objectId: 'Person2',
|
||||
score: 12,
|
||||
name: 'Tom',
|
||||
};
|
||||
|
||||
const q1 = {
|
||||
objectId: {
|
||||
$eq: 'Person2',
|
||||
},
|
||||
};
|
||||
|
||||
const q2 = {
|
||||
score: {
|
||||
$eq: 12,
|
||||
},
|
||||
};
|
||||
|
||||
const q3 = {
|
||||
name: {
|
||||
$eq: 'Tom',
|
||||
},
|
||||
};
|
||||
expect(matchesQuery(obj, q1)).toBe(true);
|
||||
expect(matchesQuery(obj, q2)).toBe(true);
|
||||
expect(matchesQuery(obj, q3)).toBe(true);
|
||||
});
|
||||
|
||||
it('matches on equality queries', function () {
|
||||
const day = new Date();
|
||||
const location = new Parse.GeoPoint({
|
||||
|
||||
@@ -1,519 +0,0 @@
|
||||
const RedisCacheAdapter = require('../lib/Adapters/Cache/RedisCacheAdapter').default;
|
||||
describe('rate limit', () => {
|
||||
it('can limit cloud functions', async () => {
|
||||
Parse.Cloud.define('test', () => 'Abc');
|
||||
await reconfigureServer({
|
||||
rateLimit: [
|
||||
{
|
||||
requestPath: '/functions/*',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
const response1 = await Parse.Cloud.run('test');
|
||||
expect(response1).toBe('Abc');
|
||||
await expectAsync(Parse.Cloud.run('test')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can limit cloud functions with user session token', async () => {
|
||||
await Parse.User.signUp('myUser', 'password');
|
||||
Parse.Cloud.define('test', () => 'Abc');
|
||||
await reconfigureServer({
|
||||
rateLimit: [
|
||||
{
|
||||
requestPath: '/functions/*',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
const response1 = await Parse.Cloud.run('test');
|
||||
expect(response1).toBe('Abc');
|
||||
await expectAsync(Parse.Cloud.run('test')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can add global limit', async () => {
|
||||
Parse.Cloud.define('test', () => 'Abc');
|
||||
await reconfigureServer({
|
||||
rateLimit: {
|
||||
requestPath: '*',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
});
|
||||
const response1 = await Parse.Cloud.run('test');
|
||||
expect(response1).toBe('Abc');
|
||||
await expectAsync(Parse.Cloud.run('test')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
await expectAsync(new Parse.Object('Test').save()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can limit cloud with validator', async () => {
|
||||
Parse.Cloud.define('test', () => 'Abc', {
|
||||
rateLimit: {
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
});
|
||||
const response1 = await Parse.Cloud.run('test');
|
||||
expect(response1).toBe('Abc');
|
||||
await expectAsync(Parse.Cloud.run('test')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can skip with masterKey', async () => {
|
||||
Parse.Cloud.define('test', () => 'Abc');
|
||||
await reconfigureServer({
|
||||
rateLimit: [
|
||||
{
|
||||
requestPath: '/functions/*',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
const response1 = await Parse.Cloud.run('test', null, { useMasterKey: true });
|
||||
expect(response1).toBe('Abc');
|
||||
const response2 = await Parse.Cloud.run('test', null, { useMasterKey: true });
|
||||
expect(response2).toBe('Abc');
|
||||
});
|
||||
|
||||
it('should run with masterKey', async () => {
|
||||
Parse.Cloud.define('test', () => 'Abc');
|
||||
await reconfigureServer({
|
||||
rateLimit: [
|
||||
{
|
||||
requestPath: '/functions/*',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
includeMasterKey: true,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
const response1 = await Parse.Cloud.run('test', null, { useMasterKey: true });
|
||||
expect(response1).toBe('Abc');
|
||||
await expectAsync(Parse.Cloud.run('test')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can limit saving objects', async () => {
|
||||
await reconfigureServer({
|
||||
rateLimit: [
|
||||
{
|
||||
requestPath: '/classes/*',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
const obj = new Parse.Object('Test');
|
||||
await obj.save();
|
||||
await expectAsync(obj.save()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can set method to post', async () => {
|
||||
await reconfigureServer({
|
||||
rateLimit: [
|
||||
{
|
||||
requestPath: '/classes/*',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
requestMethods: 'POST',
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
const obj = new Parse.Object('Test');
|
||||
await obj.save();
|
||||
await obj.save();
|
||||
const obj2 = new Parse.Object('Test');
|
||||
await expectAsync(obj2.save()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can use a validator for post', async () => {
|
||||
Parse.Cloud.beforeSave('Test', () => {}, {
|
||||
rateLimit: {
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
});
|
||||
const obj = new Parse.Object('Test');
|
||||
await obj.save();
|
||||
await expectAsync(obj.save()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can use a validator for file', async () => {
|
||||
Parse.Cloud.beforeSave(Parse.File, () => {}, {
|
||||
rateLimit: {
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
});
|
||||
const file = new Parse.File('yolo.txt', [1, 2, 3], 'text/plain');
|
||||
await file.save();
|
||||
const file2 = new Parse.File('yolo.txt', [1, 2, 3], 'text/plain');
|
||||
await expectAsync(file2.save()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can set method to get', async () => {
|
||||
await reconfigureServer({
|
||||
rateLimit: [
|
||||
{
|
||||
requestPath: '/classes/Test',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
requestMethods: 'GET',
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
const obj = new Parse.Object('Test');
|
||||
await obj.save();
|
||||
await obj.save();
|
||||
await new Parse.Query('Test').first();
|
||||
await expectAsync(new Parse.Query('Test').first()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can use a validator', async () => {
|
||||
await reconfigureServer({ silent: false });
|
||||
Parse.Cloud.beforeFind('TestObject', () => {}, {
|
||||
rateLimit: {
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
});
|
||||
const obj = new Parse.Object('TestObject');
|
||||
await obj.save();
|
||||
await obj.save();
|
||||
await new Parse.Query('TestObject').first();
|
||||
await expectAsync(new Parse.Query('TestObject').first()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
await expectAsync(new Parse.Query('TestObject').get('abc')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can set method to delete', async () => {
|
||||
await reconfigureServer({
|
||||
rateLimit: [
|
||||
{
|
||||
requestPath: '/classes/Test/*',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
requestMethods: 'DELETE',
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
const obj = new Parse.Object('Test');
|
||||
await obj.save();
|
||||
await obj.destroy();
|
||||
await expectAsync(obj.destroy()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can set beforeDelete', async () => {
|
||||
const obj = new Parse.Object('TestDelete');
|
||||
await obj.save();
|
||||
Parse.Cloud.beforeDelete('TestDelete', () => {}, {
|
||||
rateLimit: {
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
});
|
||||
await obj.destroy();
|
||||
await expectAsync(obj.destroy()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can set beforeLogin', async () => {
|
||||
Parse.Cloud.beforeLogin(() => {}, {
|
||||
rateLimit: {
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
});
|
||||
await Parse.User.signUp('myUser', 'password');
|
||||
await Parse.User.logIn('myUser', 'password');
|
||||
await expectAsync(Parse.User.logIn('myUser', 'password')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can define limits via rateLimit and define', async () => {
|
||||
await reconfigureServer({
|
||||
rateLimit: [
|
||||
{
|
||||
requestPath: '/functions/*',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 100,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
Parse.Cloud.define('test', () => 'Abc', {
|
||||
rateLimit: {
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
});
|
||||
const response1 = await Parse.Cloud.run('test');
|
||||
expect(response1).toBe('Abc');
|
||||
await expectAsync(Parse.Cloud.run('test')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests.')
|
||||
);
|
||||
});
|
||||
|
||||
it('does not limit internal calls', async () => {
|
||||
await reconfigureServer({
|
||||
rateLimit: [
|
||||
{
|
||||
requestPath: '/functions/*',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
},
|
||||
],
|
||||
});
|
||||
Parse.Cloud.define('test1', () => 'Abc');
|
||||
Parse.Cloud.define('test2', async () => {
|
||||
await Parse.Cloud.run('test1');
|
||||
await Parse.Cloud.run('test1');
|
||||
});
|
||||
await Parse.Cloud.run('test2');
|
||||
});
|
||||
|
||||
describe('zone', () => {
|
||||
const middlewares = require('../lib/middlewares');
|
||||
it('can use global zone', async () => {
|
||||
await reconfigureServer({
|
||||
rateLimit: {
|
||||
requestPath: '*',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
zone: Parse.Server.RateLimitZone.global,
|
||||
},
|
||||
});
|
||||
const fakeReq = {
|
||||
originalUrl: 'http://example.com/parse/',
|
||||
url: 'http://example.com/',
|
||||
body: {
|
||||
_ApplicationId: 'test',
|
||||
},
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
get: key => {
|
||||
return fakeReq.headers[key];
|
||||
},
|
||||
};
|
||||
fakeReq.ip = '127.0.0.1';
|
||||
let fakeRes = jasmine.createSpyObj('fakeRes', ['end', 'status', 'setHeader', 'json']);
|
||||
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
|
||||
fakeReq.ip = '127.0.0.2';
|
||||
fakeRes = jasmine.createSpyObj('fakeRes', ['end', 'status', 'setHeader']);
|
||||
let resolvingPromise;
|
||||
const promise = new Promise(resolve => {
|
||||
resolvingPromise = resolve;
|
||||
});
|
||||
fakeRes.json = jasmine.createSpy('json').and.callFake(resolvingPromise);
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||
throw 'Should not call next';
|
||||
});
|
||||
await promise;
|
||||
expect(fakeRes.status).toHaveBeenCalledWith(429);
|
||||
expect(fakeRes.json).toHaveBeenCalledWith({
|
||||
code: Parse.Error.CONNECTION_FAILED,
|
||||
error: 'Too many requests',
|
||||
});
|
||||
});
|
||||
|
||||
it('can use session zone', async () => {
|
||||
await reconfigureServer({
|
||||
rateLimit: {
|
||||
requestPath: '/functions/*',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
zone: Parse.Server.RateLimitZone.session,
|
||||
},
|
||||
});
|
||||
Parse.Cloud.define('test', () => 'Abc');
|
||||
await Parse.User.signUp('username', 'password');
|
||||
await Parse.Cloud.run('test');
|
||||
await expectAsync(Parse.Cloud.run('test')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
await Parse.User.logIn('username', 'password');
|
||||
await Parse.Cloud.run('test');
|
||||
});
|
||||
|
||||
it('can use user zone', async () => {
|
||||
await reconfigureServer({
|
||||
rateLimit: {
|
||||
requestPath: '/functions/*',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
zone: Parse.Server.RateLimitZone.user,
|
||||
},
|
||||
});
|
||||
Parse.Cloud.define('test', () => 'Abc');
|
||||
await Parse.User.signUp('username', 'password');
|
||||
await Parse.Cloud.run('test');
|
||||
await expectAsync(Parse.Cloud.run('test')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
await Parse.User.logIn('username', 'password');
|
||||
await expectAsync(Parse.Cloud.run('test')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('can validate rateLimit', async () => {
|
||||
const Config = require('../lib/Config');
|
||||
const validateRateLimit = ({ rateLimit }) => Config.validateRateLimit(rateLimit);
|
||||
expect(() =>
|
||||
validateRateLimit({ rateLimit: 'a', requestTimeWindow: 1000, requestCount: 3 })
|
||||
).toThrow('rateLimit must be an array or object');
|
||||
expect(() => validateRateLimit({ rateLimit: ['a'] })).toThrow(
|
||||
'rateLimit must be an array of objects'
|
||||
);
|
||||
expect(() => validateRateLimit({ rateLimit: [{ requestPath: [] }] })).toThrow(
|
||||
'rateLimit.requestPath must be a string'
|
||||
);
|
||||
expect(() =>
|
||||
validateRateLimit({ rateLimit: [{ requestTimeWindow: [], requestPath: 'a' }] })
|
||||
).toThrow('rateLimit.requestTimeWindow must be a number');
|
||||
expect(() =>
|
||||
validateRateLimit({
|
||||
rateLimit: [{ requestPath: 'a', requestTimeWindow: 1000, requestCount: 3, zone: 'abc' }],
|
||||
})
|
||||
).toThrow('rateLimit.zone must be one of global, session, user, or ip');
|
||||
expect(() =>
|
||||
validateRateLimit({
|
||||
rateLimit: [
|
||||
{
|
||||
includeInternalRequests: [],
|
||||
requestTimeWindow: 1000,
|
||||
requestCount: 3,
|
||||
requestPath: 'a',
|
||||
},
|
||||
],
|
||||
})
|
||||
).toThrow('rateLimit.includeInternalRequests must be a boolean');
|
||||
expect(() =>
|
||||
validateRateLimit({
|
||||
rateLimit: [{ requestCount: [], requestTimeWindow: 1000, requestPath: 'a' }],
|
||||
})
|
||||
).toThrow('rateLimit.requestCount must be a number');
|
||||
expect(() =>
|
||||
validateRateLimit({
|
||||
rateLimit: [
|
||||
{ errorResponseMessage: [], requestTimeWindow: 1000, requestCount: 3, requestPath: 'a' },
|
||||
],
|
||||
})
|
||||
).toThrow('rateLimit.errorResponseMessage must be a string');
|
||||
expect(() =>
|
||||
validateRateLimit({ rateLimit: [{ requestCount: 3, requestPath: 'abc' }] })
|
||||
).toThrow('rateLimit.requestTimeWindow must be defined');
|
||||
expect(() =>
|
||||
validateRateLimit({ rateLimit: [{ requestTimeWindow: 3, requestPath: 'abc' }] })
|
||||
).toThrow('rateLimit.requestCount must be defined');
|
||||
expect(() =>
|
||||
validateRateLimit({ rateLimit: [{ requestTimeWindow: 3, requestCount: 'abc' }] })
|
||||
).toThrow('rateLimit.requestPath must be defined');
|
||||
await expectAsync(
|
||||
reconfigureServer({
|
||||
rateLimit: [{ requestTimeWindow: 3, requestCount: 1, path: 'abc', requestPath: 'a' }],
|
||||
})
|
||||
).toBeRejectedWith(`Invalid rate limit option "path"`);
|
||||
});
|
||||
describe_only(() => {
|
||||
return process.env.PARSE_SERVER_TEST_CACHE === 'redis';
|
||||
})('with RedisCache', function () {
|
||||
it('does work with cache', async () => {
|
||||
await reconfigureServer({
|
||||
rateLimit: [
|
||||
{
|
||||
requestPath: '/classes/*',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
redisUrl: 'redis://localhost:6379',
|
||||
},
|
||||
],
|
||||
});
|
||||
const obj = new Parse.Object('Test');
|
||||
await obj.save();
|
||||
await expectAsync(obj.save()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
const cache = new RedisCacheAdapter();
|
||||
await cache.connect();
|
||||
const value = await cache.get('rl:127.0.0.1');
|
||||
expect(value).toEqual(2);
|
||||
const ttl = await cache.client.ttl('rl:127.0.0.1');
|
||||
expect(ttl).toEqual(10);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -625,7 +625,7 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
spyOn(Collection.prototype, 'aggregate').and.callThrough();
|
||||
// Query
|
||||
const query = new Parse.Query('MyObject');
|
||||
const results = await query.aggregate([{ $match: { boolKey: false } }]);
|
||||
const results = await query.aggregate([{ match: { boolKey: false } }]);
|
||||
// Validate
|
||||
expect(results.length).toBe(1);
|
||||
let readPreference = null;
|
||||
@@ -678,7 +678,7 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
// Query
|
||||
const query = new Parse.Query('MyObject');
|
||||
query.readPreference('SECONDARY');
|
||||
const results = await query.aggregate([{ $match: { boolKey: false } }]);
|
||||
const results = await query.aggregate([{ match: { boolKey: false } }]);
|
||||
// Validate
|
||||
expect(results.length).toBe(1);
|
||||
let readPreference = null;
|
||||
|
||||
@@ -19,22 +19,23 @@ describe_only(() => {
|
||||
|
||||
beforeEach(async () => {
|
||||
cache = new RedisCacheAdapter(null, 100);
|
||||
await cache.connect();
|
||||
await cache.clear();
|
||||
});
|
||||
|
||||
it('should get/set/clear', async () => {
|
||||
it('should get/set/clear', done => {
|
||||
const cacheNaN = new RedisCacheAdapter({
|
||||
ttl: NaN,
|
||||
});
|
||||
await cacheNaN.connect();
|
||||
await cacheNaN.put(KEY, VALUE);
|
||||
let value = await cacheNaN.get(KEY);
|
||||
expect(value).toEqual(VALUE);
|
||||
await cacheNaN.clear();
|
||||
value = await cacheNaN.get(KEY);
|
||||
expect(value).toEqual(null);
|
||||
await cacheNaN.clear();
|
||||
|
||||
cacheNaN
|
||||
.put(KEY, VALUE)
|
||||
.then(() => cacheNaN.get(KEY))
|
||||
.then(value => expect(value).toEqual(VALUE))
|
||||
.then(() => cacheNaN.clear())
|
||||
.then(() => cacheNaN.get(KEY))
|
||||
.then(value => expect(value).toEqual(null))
|
||||
.then(() => cacheNaN.clear())
|
||||
.then(done);
|
||||
});
|
||||
|
||||
it('should expire after ttl', done => {
|
||||
@@ -99,7 +100,7 @@ describe_only(() => {
|
||||
it('handleShutdown, close connection', async () => {
|
||||
await cache.handleShutdown();
|
||||
setTimeout(() => {
|
||||
expect(cache.client.isOpen).toBe(false);
|
||||
expect(cache.client.connected).toBe(false);
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
@@ -121,9 +122,8 @@ describe_only(() => {
|
||||
return Object.keys(cache.queue.queue).length;
|
||||
}
|
||||
|
||||
it('it should clear completed operations from queue', async done => {
|
||||
it('it should clear completed operations from queue', done => {
|
||||
const cache = new RedisCacheAdapter({ ttl: NaN });
|
||||
await cache.connect();
|
||||
|
||||
// execute a bunch of operations in sequence
|
||||
let promise = Promise.resolve();
|
||||
@@ -144,9 +144,8 @@ describe_only(() => {
|
||||
promise.then(() => expect(getQueueCount(cache)).toEqual(0)).then(done);
|
||||
});
|
||||
|
||||
it('it should count per key chained operations correctly', async done => {
|
||||
it('it should count per key chained operations correctly', done => {
|
||||
const cache = new RedisCacheAdapter({ ttl: NaN });
|
||||
await cache.connect();
|
||||
|
||||
let key1Promise = Promise.resolve();
|
||||
let key2Promise = Promise.resolve();
|
||||
@@ -167,18 +166,4 @@ describe_only(() => {
|
||||
.then(() => expect(getQueueCount(cache)).toEqual(0))
|
||||
.then(done);
|
||||
});
|
||||
|
||||
it('should start and connect cache adapter', async () => {
|
||||
const server = await reconfigureServer({
|
||||
cacheAdapter: {
|
||||
module: `${__dirname.replace('/spec', '')}/lib/Adapters/Cache/RedisCacheAdapter`,
|
||||
options: {
|
||||
url: 'redis://127.0.0.1:6379/1',
|
||||
},
|
||||
},
|
||||
});
|
||||
const symbol = Object.getOwnPropertySymbols(server.config.cacheController);
|
||||
const client = server.config.cacheController[symbol[0]].client;
|
||||
expect(client.isOpen).toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,8 +15,7 @@ describe('RedisPubSub', function () {
|
||||
});
|
||||
|
||||
const redis = require('redis');
|
||||
expect(redis.createClient).toHaveBeenCalledWith({
|
||||
url: 'redisAddress',
|
||||
expect(redis.createClient).toHaveBeenCalledWith('redisAddress', {
|
||||
socket_keepalive: true,
|
||||
no_ready_check: true,
|
||||
});
|
||||
@@ -29,8 +28,7 @@ describe('RedisPubSub', function () {
|
||||
});
|
||||
|
||||
const redis = require('redis');
|
||||
expect(redis.createClient).toHaveBeenCalledWith({
|
||||
url: 'redisAddress',
|
||||
expect(redis.createClient).toHaveBeenCalledWith('redisAddress', {
|
||||
socket_keepalive: true,
|
||||
no_ready_check: true,
|
||||
});
|
||||
|
||||
@@ -19,7 +19,6 @@ const publicServerURL = 'http://localhost:8378/1';
|
||||
describe('Regex Vulnerabilities', function () {
|
||||
beforeEach(async function () {
|
||||
await reconfigureServer({
|
||||
maintenanceKey: 'test2',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter,
|
||||
appName,
|
||||
@@ -99,20 +98,11 @@ describe('Regex Vulnerabilities', function () {
|
||||
|
||||
it('should work with plain token', async function () {
|
||||
expect(this.user.get('emailVerified')).toEqual(false);
|
||||
const current = await request({
|
||||
method: 'GET',
|
||||
url: `http://localhost:8378/1/classes/_User/${this.user.id}`,
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Rest-API-Key': 'test',
|
||||
'X-Parse-Maintenance-Key': 'test2',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}).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?username=someemail@somedomain.com&token=${this.user.get(
|
||||
'_email_verify_token'
|
||||
)}`,
|
||||
method: 'GET',
|
||||
});
|
||||
await this.user.fetch({ useMasterKey: true });
|
||||
@@ -174,18 +164,8 @@ describe('Regex Vulnerabilities', function () {
|
||||
email: 'someemail@somedomain.com',
|
||||
}),
|
||||
});
|
||||
const current = await request({
|
||||
method: 'GET',
|
||||
url: `http://localhost:8378/1/classes/_User/${this.user.id}`,
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Rest-API-Key': 'test',
|
||||
'X-Parse-Maintenance-Key': 'test2',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}).then(res => res.data);
|
||||
const token = current._perishable_token;
|
||||
await this.user.fetch({ useMasterKey: true });
|
||||
const token = this.user.get('_perishable_token');
|
||||
const passwordResetResponse = await request({
|
||||
url: `${serverURL}/apps/test/request_password_reset?username=someemail@somedomain.com&token=${token}`,
|
||||
method: 'GET',
|
||||
|
||||
+20
-25
@@ -96,7 +96,6 @@ describe('rest query', () => {
|
||||
let user = {
|
||||
username: 'aUsername',
|
||||
password: 'aPassword',
|
||||
ACL: { '*': { read: true } },
|
||||
};
|
||||
const activity = {
|
||||
type: 'comment',
|
||||
@@ -399,16 +398,15 @@ describe('RestQuery.each', () => {
|
||||
}
|
||||
const config = Config.get('test');
|
||||
await Parse.Object.saveAll(objects);
|
||||
const query = await RestQuery({
|
||||
method: RestQuery.Method.find,
|
||||
const query = new RestQuery(
|
||||
config,
|
||||
auth: auth.master(config),
|
||||
className: 'Object',
|
||||
restWhere: { value: { $gt: 2 } },
|
||||
restOptions: { limit: 2 },
|
||||
});
|
||||
auth.master(config),
|
||||
'Object',
|
||||
{ value: { $gt: 2 } },
|
||||
{ limit: 2 }
|
||||
);
|
||||
const spy = spyOn(query, 'execute').and.callThrough();
|
||||
const classSpy = spyOn(RestQuery._UnsafeRestQuery.prototype, 'execute').and.callThrough();
|
||||
const classSpy = spyOn(RestQuery.prototype, 'execute').and.callThrough();
|
||||
const results = [];
|
||||
await query.each(result => {
|
||||
expect(result.value).toBeGreaterThan(2);
|
||||
@@ -439,37 +437,34 @@ describe('RestQuery.each', () => {
|
||||
* Two queries needed since objectId are sorted and we can't know which one
|
||||
* going to be the first and then skip by the $gt added by each
|
||||
*/
|
||||
const queryOne = await RestQuery({
|
||||
method: RestQuery.Method.get,
|
||||
const queryOne = new RestQuery(
|
||||
config,
|
||||
auth: auth.master(config),
|
||||
className: 'Letter',
|
||||
restWhere: {
|
||||
auth.master(config),
|
||||
'Letter',
|
||||
{
|
||||
numbers: {
|
||||
__type: 'Pointer',
|
||||
className: 'Number',
|
||||
objectId: object1.id,
|
||||
},
|
||||
},
|
||||
restOptions: { limit: 1 },
|
||||
});
|
||||
|
||||
const queryTwo = await RestQuery({
|
||||
method: RestQuery.Method.get,
|
||||
{ limit: 1 }
|
||||
);
|
||||
const queryTwo = new RestQuery(
|
||||
config,
|
||||
auth: auth.master(config),
|
||||
className: 'Letter',
|
||||
restWhere: {
|
||||
auth.master(config),
|
||||
'Letter',
|
||||
{
|
||||
numbers: {
|
||||
__type: 'Pointer',
|
||||
className: 'Number',
|
||||
objectId: object2.id,
|
||||
},
|
||||
},
|
||||
restOptions: { limit: 1 },
|
||||
});
|
||||
{ limit: 1 }
|
||||
);
|
||||
|
||||
const classSpy = spyOn(RestQuery._UnsafeRestQuery.prototype, 'execute').and.callThrough();
|
||||
const classSpy = spyOn(RestQuery.prototype, 'execute').and.callThrough();
|
||||
const resultsOne = [];
|
||||
const resultsTwo = [];
|
||||
await queryOne.each(result => {
|
||||
|
||||
@@ -204,58 +204,4 @@ describe('Schema Performance', function () {
|
||||
);
|
||||
expect(getAllSpy.calls.count()).toBe(2);
|
||||
});
|
||||
|
||||
it('does reload with schemaCacheTtl', async () => {
|
||||
const databaseURI =
|
||||
process.env.PARSE_SERVER_TEST_DB === 'postgres'
|
||||
? process.env.PARSE_SERVER_TEST_DATABASE_URI
|
||||
: 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase';
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI,
|
||||
silent: false,
|
||||
databaseOptions: { schemaCacheTtl: 1000 },
|
||||
});
|
||||
const SchemaController = require('../lib/Controllers/SchemaController').SchemaController;
|
||||
const spy = spyOn(SchemaController.prototype, 'reloadData').and.callThrough();
|
||||
Object.defineProperty(spy, 'reloadCalls', {
|
||||
get: () => spy.calls.all().filter(call => call.args[0].clearCache).length,
|
||||
});
|
||||
|
||||
const object = new TestObject();
|
||||
object.set('foo', 'bar');
|
||||
await object.save();
|
||||
|
||||
spy.calls.reset();
|
||||
|
||||
object.set('foo', 'bar');
|
||||
await object.save();
|
||||
|
||||
expect(spy.reloadCalls).toBe(0);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1100));
|
||||
|
||||
object.set('foo', 'bar');
|
||||
await object.save();
|
||||
|
||||
expect(spy.reloadCalls).toBe(1);
|
||||
});
|
||||
|
||||
it('cannot set invalid databaseOptions', async () => {
|
||||
const expectError = async (key, value, expected) =>
|
||||
expectAsync(
|
||||
reconfigureServer({ databaseAdapter: undefined, databaseOptions: { [key]: value } })
|
||||
).toBeRejectedWith(`databaseOptions.${key} must be a ${expected}`);
|
||||
for (const databaseOptions of [[], 0, 'string']) {
|
||||
await expectAsync(
|
||||
reconfigureServer({ databaseAdapter: undefined, databaseOptions })
|
||||
).toBeRejectedWith(`databaseOptions must be an object`);
|
||||
}
|
||||
for (const value of [null, 0, 'string', {}, []]) {
|
||||
await expectError('enableSchemaHooks', value, 'boolean');
|
||||
}
|
||||
for (const value of [null, false, 'string', {}, []]) {
|
||||
await expectError('schemaCacheTtl', value, 'number');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -337,33 +337,5 @@ describe('Security Check', () => {
|
||||
expect(logSpy.calls.all()[0].args[0]).toContain(title);
|
||||
}
|
||||
});
|
||||
|
||||
it('does update featuresRouter', async () => {
|
||||
let response = await request({
|
||||
url: 'http://localhost:8378/1/serverInfo',
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
},
|
||||
});
|
||||
expect(response.data.features.settings.securityCheck).toBeTrue();
|
||||
await reconfigureServer({
|
||||
security: {
|
||||
enableCheck: false,
|
||||
},
|
||||
});
|
||||
response = await request({
|
||||
url: 'http://localhost:8378/1/serverInfo',
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
},
|
||||
});
|
||||
expect(response.data.features.settings.securityCheck).toBeFalse();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,7 +47,7 @@ describe('SessionTokenCache', function () {
|
||||
|
||||
sessionTokenCache.getUserId('sessionToken').then(userIdFromCache => {
|
||||
expect(userIdFromCache).toBe('userId');
|
||||
expect(sessionTokenCache.cache.size).toBe(1);
|
||||
expect(sessionTokenCache.cache.length).toBe(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
+27
-48
@@ -1,76 +1,55 @@
|
||||
const UserController = require('../lib/Controllers/UserController').UserController;
|
||||
const emailAdapter = require('./support/MockEmailAdapter');
|
||||
const Config = require('../lib/Config');
|
||||
const Auth = require('../lib/Auth');
|
||||
|
||||
describe('UserController', () => {
|
||||
const user = {
|
||||
_email_verify_token: 'testToken',
|
||||
username: 'testUser',
|
||||
email: 'test@example.com',
|
||||
};
|
||||
|
||||
describe('sendVerificationEmail', () => {
|
||||
describe('parseFrameURL not provided', () => {
|
||||
it('uses publicServerURL', async () => {
|
||||
it('uses publicServerURL', async done => {
|
||||
await reconfigureServer({
|
||||
publicServerURL: 'http://www.example.com',
|
||||
customPages: {
|
||||
parseFrameURL: undefined,
|
||||
},
|
||||
verifyUserEmails: true,
|
||||
emailAdapter,
|
||||
appName: 'test',
|
||||
});
|
||||
|
||||
let emailOptions;
|
||||
emailAdapter.sendVerificationEmail = options => {
|
||||
emailOptions = options;
|
||||
return Promise.resolve();
|
||||
expect(options.link).toEqual(
|
||||
'http://www.example.com/apps/test/verify_email?token=testToken&username=testUser'
|
||||
);
|
||||
emailAdapter.sendVerificationEmail = () => Promise.resolve();
|
||||
done();
|
||||
};
|
||||
|
||||
const username = 'verificationUser';
|
||||
const user = new Parse.User();
|
||||
user.setUsername(username);
|
||||
user.setPassword('pass');
|
||||
user.setEmail('verification@example.com');
|
||||
await user.signUp();
|
||||
|
||||
const config = Config.get('test');
|
||||
const rawUser = await config.database.find('_User', { username }, {}, Auth.maintenance(config));
|
||||
const rawUsername = rawUser[0].username;
|
||||
const rawToken = rawUser[0]._email_verify_token;
|
||||
expect(rawToken).toBeDefined();
|
||||
expect(rawUsername).toBe(username);
|
||||
expect(emailOptions.link).toEqual(`http://www.example.com/apps/test/verify_email?token=${rawToken}&username=${username}`);
|
||||
const userController = new UserController(emailAdapter, 'test', {
|
||||
verifyUserEmails: true,
|
||||
});
|
||||
userController.sendVerificationEmail(user);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseFrameURL provided', () => {
|
||||
it('uses parseFrameURL and includes the destination in the link parameter', async () => {
|
||||
it('uses parseFrameURL and includes the destination in the link parameter', async done => {
|
||||
await reconfigureServer({
|
||||
publicServerURL: 'http://www.example.com',
|
||||
customPages: {
|
||||
parseFrameURL: 'http://someother.example.com/handle-parse-iframe',
|
||||
},
|
||||
verifyUserEmails: true,
|
||||
emailAdapter,
|
||||
appName: 'test',
|
||||
});
|
||||
|
||||
let emailOptions;
|
||||
emailAdapter.sendVerificationEmail = options => {
|
||||
emailOptions = options;
|
||||
return Promise.resolve();
|
||||
expect(options.link).toEqual(
|
||||
'http://someother.example.com/handle-parse-iframe?link=%2Fapps%2Ftest%2Fverify_email&token=testToken&username=testUser'
|
||||
);
|
||||
emailAdapter.sendVerificationEmail = () => Promise.resolve();
|
||||
done();
|
||||
};
|
||||
|
||||
const username = 'verificationUser';
|
||||
const user = new Parse.User();
|
||||
user.setUsername(username);
|
||||
user.setPassword('pass');
|
||||
user.setEmail('verification@example.com');
|
||||
await user.signUp();
|
||||
|
||||
const config = Config.get('test');
|
||||
const rawUser = await config.database.find('_User', { username }, {}, Auth.maintenance(config));
|
||||
const rawUsername = rawUser[0].username;
|
||||
const rawToken = rawUser[0]._email_verify_token;
|
||||
expect(rawToken).toBeDefined();
|
||||
expect(rawUsername).toBe(username);
|
||||
expect(emailOptions.link).toEqual(`http://someother.example.com/handle-parse-iframe?link=%2Fapps%2Ftest%2Fverify_email&token=${rawToken}&username=${username}`);
|
||||
const userController = new UserController(emailAdapter, 'test', {
|
||||
verifyUserEmails: true,
|
||||
});
|
||||
userController.sendVerificationEmail(user);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,9 +16,7 @@ describe('Personally Identifiable Information', () => {
|
||||
await reconfigureServer();
|
||||
user = await Parse.User.signUp('tester', 'abc');
|
||||
user = await Parse.User.logIn(user.get('username'), 'abc');
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
await user.set('email', EMAIL).set('zip', ZIP).set('ssn', SSN).setACL(acl).save();
|
||||
await user.set('email', EMAIL).set('zip', ZIP).set('ssn', SSN).save();
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
const Utils = require('../src/Utils');
|
||||
|
||||
describe('Utils', () => {
|
||||
describe('addNestedKeysToRoot', () => {
|
||||
it('should move the nested keys to root of object', async () => {
|
||||
const obj = {
|
||||
a: 1,
|
||||
b: {
|
||||
c: 2,
|
||||
d: 3
|
||||
},
|
||||
e: 4
|
||||
};
|
||||
Utils.addNestedKeysToRoot(obj, 'b');
|
||||
expect(obj).toEqual({
|
||||
a: 1,
|
||||
c: 2,
|
||||
d: 3,
|
||||
e: 4
|
||||
});
|
||||
});
|
||||
|
||||
it('should not modify the object if the key does not exist', async () => {
|
||||
const obj = {
|
||||
a: 1,
|
||||
e: 4
|
||||
};
|
||||
Utils.addNestedKeysToRoot(obj, 'b');
|
||||
expect(obj).toEqual({
|
||||
a: 1,
|
||||
e: 4
|
||||
});
|
||||
});
|
||||
|
||||
it('should not modify the object if the key is not an object', () => {
|
||||
const obj = {
|
||||
a: 1,
|
||||
b: 2,
|
||||
e: 4
|
||||
};
|
||||
Utils.addNestedKeysToRoot(obj, 'b');
|
||||
expect(obj).toEqual({
|
||||
a: 1,
|
||||
b: 2,
|
||||
e: 4
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -51,7 +51,6 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
user.setUsername('zxcv');
|
||||
user.setEmail('testIfEnabled@parse.com');
|
||||
await user.signUp();
|
||||
await jasmine.timeout();
|
||||
expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled();
|
||||
user.fetch().then(() => {
|
||||
expect(user.get('emailVerified')).toEqual(false);
|
||||
@@ -138,7 +137,6 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
spyOn(emailAdapter, 'sendVerificationEmail').and.callFake(options => {
|
||||
expect(options.link).not.toBeNull();
|
||||
expect(options.link).not.toMatch(/token=undefined/);
|
||||
expect(options.link).not.toMatch(/username=undefined/);
|
||||
Promise.resolve();
|
||||
});
|
||||
const user = new Parse.User();
|
||||
@@ -185,7 +183,6 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
user.setUsername('zxcv');
|
||||
user.set('email', 'testSendSimpleAdapter@parse.com');
|
||||
await user.signUp();
|
||||
await jasmine.timeout();
|
||||
expect(calls).toBe(1);
|
||||
user
|
||||
.fetch()
|
||||
@@ -245,67 +242,8 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('prevents user from signup and login if email is not verified and preventLoginWithUnverifiedEmail is set to function returning true', async () => {
|
||||
await reconfigureServer({
|
||||
appName: 'test',
|
||||
publicServerURL: 'http://localhost:1337/1',
|
||||
verifyUserEmails: async () => true,
|
||||
preventLoginWithUnverifiedEmail: async () => true,
|
||||
preventSignupWithUnverifiedEmail: true,
|
||||
emailAdapter: MockEmailAdapterWithOptions({
|
||||
fromAddress: 'parse@example.com',
|
||||
apiKey: 'k',
|
||||
domain: 'd',
|
||||
}),
|
||||
});
|
||||
|
||||
it('allows user to login only after user clicks on the link to confirm email address if preventLoginWithUnverifiedEmail is set to true', done => {
|
||||
const user = new Parse.User();
|
||||
user.setPassword('asdf');
|
||||
user.setUsername('zxcv');
|
||||
user.set('email', 'testInvalidConfig@parse.com');
|
||||
const signupRes = await user.signUp(null).catch(e => e);
|
||||
expect(signupRes.message).toEqual('User email is not verified.');
|
||||
|
||||
const loginRes = await Parse.User.logIn('zxcv', 'asdf').catch(e => e);
|
||||
expect(loginRes.message).toEqual('User email is not verified.');
|
||||
});
|
||||
|
||||
it('provides function arguments in verifyUserEmails on login', async () => {
|
||||
const user = new Parse.User();
|
||||
user.setUsername('user');
|
||||
user.setPassword('pass');
|
||||
user.set('email', 'test@example.com');
|
||||
await user.signUp();
|
||||
|
||||
const verifyUserEmails = {
|
||||
method: async (params) => {
|
||||
expect(params.object).toBeInstanceOf(Parse.User);
|
||||
expect(params.ip).toBeDefined();
|
||||
expect(params.master).toBeDefined();
|
||||
expect(params.installationId).toBeDefined();
|
||||
return true;
|
||||
},
|
||||
};
|
||||
const verifyUserEmailsSpy = spyOn(verifyUserEmails, 'method').and.callThrough();
|
||||
await reconfigureServer({
|
||||
appName: 'test',
|
||||
publicServerURL: 'http://localhost:1337/1',
|
||||
verifyUserEmails: verifyUserEmails.method,
|
||||
preventLoginWithUnverifiedEmail: verifyUserEmails.method,
|
||||
preventSignupWithUnverifiedEmail: true,
|
||||
emailAdapter: MockEmailAdapterWithOptions({
|
||||
fromAddress: 'parse@example.com',
|
||||
apiKey: 'k',
|
||||
domain: 'd',
|
||||
}),
|
||||
});
|
||||
|
||||
const res = await Parse.User.logIn('user', 'pass').catch(e => e);
|
||||
expect(res.code).toBe(205);
|
||||
expect(verifyUserEmailsSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('allows user to login only after user clicks on the link to confirm email address if preventLoginWithUnverifiedEmail is set to true', async () => {
|
||||
let sendEmailOptions;
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: options => {
|
||||
@@ -314,33 +252,59 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {},
|
||||
};
|
||||
await reconfigureServer({
|
||||
reconfigureServer({
|
||||
appName: 'emailing app',
|
||||
verifyUserEmails: true,
|
||||
preventLoginWithUnverifiedEmail: true,
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
});
|
||||
let user = new Parse.User();
|
||||
user.setPassword('other-password');
|
||||
user.setUsername('user');
|
||||
user.set('email', 'user@example.com');
|
||||
await user.signUp();
|
||||
await jasmine.timeout();
|
||||
expect(sendEmailOptions).not.toBeUndefined();
|
||||
const response = await request({
|
||||
url: sendEmailOptions.link,
|
||||
followRedirects: false,
|
||||
});
|
||||
expect(response.status).toEqual(302);
|
||||
expect(response.text).toEqual(
|
||||
'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=user'
|
||||
);
|
||||
user = await new Parse.Query(Parse.User).first({ useMasterKey: true });
|
||||
expect(user.get('emailVerified')).toEqual(true);
|
||||
user = await Parse.User.logIn('user', 'other-password');
|
||||
expect(typeof user).toBe('object');
|
||||
expect(user.get('emailVerified')).toBe(true);
|
||||
})
|
||||
.then(() => {
|
||||
user.setPassword('other-password');
|
||||
user.setUsername('user');
|
||||
user.set('email', 'user@parse.com');
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => {
|
||||
expect(sendEmailOptions).not.toBeUndefined();
|
||||
request({
|
||||
url: sendEmailOptions.link,
|
||||
followRedirects: false,
|
||||
}).then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
expect(response.text).toEqual(
|
||||
'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=user'
|
||||
);
|
||||
user
|
||||
.fetch()
|
||||
.then(
|
||||
() => {
|
||||
expect(user.get('emailVerified')).toEqual(true);
|
||||
|
||||
Parse.User.logIn('user', 'other-password').then(
|
||||
user => {
|
||||
expect(typeof user).toBe('object');
|
||||
expect(user.get('emailVerified')).toBe(true);
|
||||
done();
|
||||
},
|
||||
() => {
|
||||
fail('login should have succeeded');
|
||||
done();
|
||||
}
|
||||
);
|
||||
},
|
||||
err => {
|
||||
jfail(err);
|
||||
fail('this should not fail');
|
||||
done();
|
||||
}
|
||||
)
|
||||
.catch(err => {
|
||||
jfail(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('allows user to login if email is not verified but preventLoginWithUnverifiedEmail is set to false', done => {
|
||||
@@ -381,35 +345,6 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('does not allow signup with preventSignupWithUnverified', async () => {
|
||||
let sendEmailOptions;
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: options => {
|
||||
sendEmailOptions = options;
|
||||
},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {},
|
||||
};
|
||||
await reconfigureServer({
|
||||
appName: 'test',
|
||||
publicServerURL: 'http://localhost:1337/1',
|
||||
verifyUserEmails: true,
|
||||
preventLoginWithUnverifiedEmail: true,
|
||||
preventSignupWithUnverifiedEmail: true,
|
||||
emailAdapter,
|
||||
});
|
||||
const newUser = new Parse.User();
|
||||
newUser.setPassword('asdf');
|
||||
newUser.setUsername('zxcv');
|
||||
newUser.set('email', 'test@example.com');
|
||||
await expectAsync(newUser.signUp()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, 'User email is not verified.')
|
||||
);
|
||||
const user = await new Parse.Query(Parse.User).first({ useMasterKey: true });
|
||||
expect(user).toBeDefined();
|
||||
expect(sendEmailOptions).toBeDefined();
|
||||
});
|
||||
|
||||
it('fails if you include an emailAdapter, set a publicServerURL, but have no appName and send a password reset email', done => {
|
||||
reconfigureServer({
|
||||
appName: undefined,
|
||||
@@ -638,7 +573,6 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
user.setUsername('zxcv');
|
||||
user.set('email', 'user@parse.com');
|
||||
await user.signUp();
|
||||
await jasmine.timeout();
|
||||
expect(emailSent).toBe(true);
|
||||
done();
|
||||
});
|
||||
@@ -666,7 +600,6 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
user.set('email', 'user@parse.com');
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => jasmine.timeout())
|
||||
.then(() => {
|
||||
expect(sendEmailOptions).not.toBeUndefined();
|
||||
request({
|
||||
@@ -1149,43 +1082,4 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw on an invalid reset password', async () => {
|
||||
await reconfigureServer({
|
||||
appName: 'coolapp',
|
||||
publicServerURL: 'http://localhost:1337/1',
|
||||
emailAdapter: MockEmailAdapterWithOptions({
|
||||
fromAddress: 'parse@example.com',
|
||||
apiKey: 'k',
|
||||
domain: 'd',
|
||||
}),
|
||||
passwordPolicy: {
|
||||
resetPasswordSuccessOnInvalidEmail: false,
|
||||
},
|
||||
});
|
||||
|
||||
await expectAsync(Parse.User.requestPasswordReset('test@example.com')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'A user with that email does not exist.')
|
||||
);
|
||||
});
|
||||
|
||||
it('validate resetPasswordSuccessonInvalidEmail', async () => {
|
||||
const invalidValues = [[], {}, 1, 'string'];
|
||||
for (const value of invalidValues) {
|
||||
await expectAsync(
|
||||
reconfigureServer({
|
||||
appName: 'coolapp',
|
||||
publicServerURL: 'http://localhost:1337/1',
|
||||
emailAdapter: MockEmailAdapterWithOptions({
|
||||
fromAddress: 'parse@example.com',
|
||||
apiKey: 'k',
|
||||
domain: 'd',
|
||||
}),
|
||||
passwordPolicy: {
|
||||
resetPasswordSuccessOnInvalidEmail: value,
|
||||
},
|
||||
})
|
||||
).toBeRejectedWith('resetPasswordSuccessOnInvalidEmail must be a boolean value');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -353,9 +353,8 @@ describe('Verify User Password', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails to verify password when preventLoginWithUnverifiedEmail is set to true REST API', async () => {
|
||||
await reconfigureServer({
|
||||
it('fails to verify password when preventLoginWithUnverifiedEmail is set to true REST API', done => {
|
||||
reconfigureServer({
|
||||
publicServerURL: 'http://localhost:8378/',
|
||||
appName: 'emailVerify',
|
||||
verifyUserEmails: true,
|
||||
@@ -365,21 +364,28 @@ describe('Verify User Password', () => {
|
||||
apiKey: 'k',
|
||||
domain: 'd',
|
||||
}),
|
||||
});
|
||||
const user = new Parse.User();
|
||||
await user.save({
|
||||
username: 'unverified-user',
|
||||
password: 'mypass',
|
||||
email: 'unverified-email@example.com',
|
||||
});
|
||||
const res = await verifyPassword('unverified-email@example.com', 'mypass', true);
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.data).toEqual({
|
||||
code: Parse.Error.EMAIL_NOT_FOUND,
|
||||
error: 'User email is not verified.',
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
const user = new Parse.User();
|
||||
return user.save({
|
||||
username: 'unverified-user',
|
||||
password: 'mypass',
|
||||
email: 'unverified-email@user.com',
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
return verifyPassword('unverified-email@user.com', 'mypass', true);
|
||||
})
|
||||
.then(res => {
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.text).toMatch('{"code":205,"error":"User email is not verified."}');
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
fail(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('verify password lock account if failed verify password attempts are above threshold', done => {
|
||||
reconfigureServer({
|
||||
appName: 'lockout threshold',
|
||||
@@ -585,83 +591,4 @@ describe('Verify User Password', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('verify password of user with unverified email with master key and ignoreEmailVerification=true', async () => {
|
||||
await reconfigureServer({
|
||||
publicServerURL: 'http://localhost:8378/',
|
||||
appName: 'emailVerify',
|
||||
verifyUserEmails: true,
|
||||
preventLoginWithUnverifiedEmail: true,
|
||||
emailAdapter: MockEmailAdapterWithOptions({
|
||||
fromAddress: 'parse@example.com',
|
||||
apiKey: 'k',
|
||||
domain: 'd',
|
||||
}),
|
||||
});
|
||||
|
||||
const user = new Parse.User();
|
||||
user.setUsername('user');
|
||||
user.setPassword('pass');
|
||||
user.setEmail('test@example.com');
|
||||
await user.signUp();
|
||||
|
||||
const { data: res } = await request({
|
||||
method: 'POST',
|
||||
url: Parse.serverURL + '/verifyPassword',
|
||||
headers: {
|
||||
'X-Parse-Master-Key': Parse.masterKey,
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: {
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
ignoreEmailVerification: true,
|
||||
},
|
||||
json: true,
|
||||
});
|
||||
expect(res.objectId).toBe(user.id);
|
||||
expect(Object.prototype.hasOwnProperty.call(res, 'sessionToken')).toEqual(false);
|
||||
expect(Object.prototype.hasOwnProperty.call(res, 'password')).toEqual(false);
|
||||
});
|
||||
|
||||
it('fails to verify password of user with unverified email with master key and ignoreEmailVerification=false', async () => {
|
||||
await reconfigureServer({
|
||||
publicServerURL: 'http://localhost:8378/',
|
||||
appName: 'emailVerify',
|
||||
verifyUserEmails: true,
|
||||
preventLoginWithUnverifiedEmail: true,
|
||||
emailAdapter: MockEmailAdapterWithOptions({
|
||||
fromAddress: 'parse@example.com',
|
||||
apiKey: 'k',
|
||||
domain: 'd',
|
||||
}),
|
||||
});
|
||||
|
||||
const user = new Parse.User();
|
||||
user.setUsername('user');
|
||||
user.setPassword('pass');
|
||||
user.setEmail('test@example.com');
|
||||
await user.signUp();
|
||||
|
||||
const res = await request({
|
||||
method: 'POST',
|
||||
url: Parse.serverURL + '/verifyPassword',
|
||||
headers: {
|
||||
'X-Parse-Master-Key': Parse.masterKey,
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: {
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
ignoreEmailVerification: false,
|
||||
},
|
||||
json: true,
|
||||
}).catch(e => e);
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.text).toMatch(/User email is not verified/);
|
||||
});
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user