mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30576f1091 | ||
|
|
e016d813e0 | ||
|
|
c8bc200729 | ||
|
|
09d04b0aad | ||
|
|
38f64be596 |
@@ -1,14 +1,13 @@
|
||||
{
|
||||
"plugins": [
|
||||
"@babel/plugin-transform-flow-strip-types"
|
||||
"@babel/plugin-transform-flow-strip-types",
|
||||
"@babel/plugin-proposal-object-rest-spread"
|
||||
],
|
||||
"presets": [
|
||||
"@babel/preset-typescript",
|
||||
["@babel/preset-env", {
|
||||
"targets": {
|
||||
"node": "18"
|
||||
},
|
||||
"exclude": ["proposal-dynamic-import"]
|
||||
"node": "12"
|
||||
}
|
||||
}]
|
||||
],
|
||||
"sourceMaps": "inline"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
*.md
|
||||
PATENTS
|
||||
LICENSE
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
.gitignore
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
lib
|
||||
coverage
|
||||
out
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": "eslint:recommended",
|
||||
"env": {
|
||||
"node": true,
|
||||
"es6": true
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"plugins": [
|
||||
"flowtype"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"indent": ["error", 2, { "SwitchCase": 1 }],
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"no-trailing-spaces": 2,
|
||||
"eol-last": 2,
|
||||
"space-in-parens": ["error", "never"],
|
||||
"no-multiple-empty-lines": 1,
|
||||
"prefer-const": "error",
|
||||
"space-infix-ops": "error",
|
||||
"no-useless-escape": "off",
|
||||
"require-atomic-updates": "off"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
+158
-167
@@ -1,17 +1,12 @@
|
||||
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: 24.11.0
|
||||
NODE_VERSION: 18.1.0
|
||||
PARSE_SERVER_TEST_TIMEOUT: 20000
|
||||
permissions:
|
||||
actions: write
|
||||
jobs:
|
||||
check-code-analysis:
|
||||
name: Code Analysis
|
||||
@@ -23,174 +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
|
||||
check-types:
|
||||
name: Check Types
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- run: npm ci
|
||||
- name: Build types
|
||||
run: npm run build:types
|
||||
- name: Test Types
|
||||
run: npm run test:types
|
||||
version: 1
|
||||
check-mongo:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- name: MongoDB 6, ReplicaSet
|
||||
MONGODB_VERSION: 6.0.19
|
||||
MONGODB_TOPOLOGY: replset
|
||||
NODE_VERSION: 24.11.0
|
||||
- name: MongoDB 7, ReplicaSet
|
||||
MONGODB_VERSION: 7.0.16
|
||||
MONGODB_TOPOLOGY: replset
|
||||
NODE_VERSION: 24.11.0
|
||||
- name: MongoDB 8, ReplicaSet
|
||||
MONGODB_VERSION: 8.0.4
|
||||
MONGODB_TOPOLOGY: replset
|
||||
NODE_VERSION: 24.11.0
|
||||
- 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: 8.0.4
|
||||
MONGODB_VERSION: 4.4.13
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
NODE_VERSION: 24.11.0
|
||||
- name: Node 20
|
||||
MONGODB_VERSION: 8.0.4
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 18.1.0
|
||||
- name: Node 12
|
||||
MONGODB_VERSION: 4.4.13
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
NODE_VERSION: 20.18.0
|
||||
- name: Node 18
|
||||
MONGODB_VERSION: 8.0.4
|
||||
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
|
||||
- name: Node 22
|
||||
MONGODB_VERSION: 8.0.4
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 14.19.1
|
||||
- name: Node 16
|
||||
MONGODB_VERSION: 4.4.13
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
NODE_VERSION: 22.12.0
|
||||
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: 20
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
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 }}
|
||||
@@ -200,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') }}
|
||||
@@ -214,41 +209,43 @@ jobs:
|
||||
${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- run: npm run coverage:mongodb
|
||||
- run: npm run pretest
|
||||
- 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 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: 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: 24.11.0
|
||||
- name: PostgreSQL 15, PostGIS 3.4
|
||||
POSTGRES_IMAGE: postgis/postgis:15-3.4
|
||||
NODE_VERSION: 24.11.0
|
||||
- name: PostgreSQL 15, PostGIS 3.5
|
||||
POSTGRES_IMAGE: postgis/postgis:15-3.5
|
||||
NODE_VERSION: 24.11.0
|
||||
- name: PostgreSQL 16, PostGIS 3.5
|
||||
POSTGRES_IMAGE: postgis/postgis:16-3.5
|
||||
NODE_VERSION: 24.11.0
|
||||
- name: PostgreSQL 17, PostGIS 3.5
|
||||
POSTGRES_IMAGE: postgis/postgis:17-3.5
|
||||
NODE_VERSION: 24.11.0
|
||||
- name: PostgreSQL 18, PostGIS 3.6
|
||||
POSTGRES_IMAGE: postgis/postgis:18-3.6
|
||||
NODE_VERSION: 24.11.0
|
||||
NODE_VERSION: 18.1.0
|
||||
fail-fast: false
|
||||
name: ${{ matrix.name }}
|
||||
timeout-minutes: 20
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-20.04
|
||||
services:
|
||||
redis:
|
||||
image: redis
|
||||
@@ -270,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') }}
|
||||
@@ -290,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.4
|
||||
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 }}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
# Trigger this workflow only to manually create a docs release; this should only be used
|
||||
# in extraordinary circumstances, as docs releases are normally created automatically as
|
||||
# part of the automated release workflow.
|
||||
|
||||
name: release-manual-docs
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
default: ''
|
||||
description: 'Reference (tag / SHA):'
|
||||
required: true
|
||||
jobs:
|
||||
docs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.ref }}
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.20.4
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
- name: Generate Docs
|
||||
run: |
|
||||
echo $SOURCE_TAG
|
||||
npm ci
|
||||
./release_docs.sh
|
||||
env:
|
||||
SOURCE_TAG: ${{ github.event.inputs.ref }}
|
||||
- name: Deploy
|
||||
uses: peaceiris/actions-gh-pages@v3.7.3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./docs
|
||||
@@ -1,43 +0,0 @@
|
||||
name: release-prepare-monthly
|
||||
on:
|
||||
schedule:
|
||||
# Runs at midnight UTC on the 1st of every month
|
||||
- cron: '0 0 1 * *'
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
create-release-pr:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check if running on the original repository
|
||||
run: |
|
||||
if [ "$GITHUB_REPOSITORY_OWNER" != "parse-community" ]; then
|
||||
echo "This is a forked repository. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
- name: Checkout working branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Compose branch name for PR
|
||||
run: echo "BRANCH_NAME=build/release-$(date +'%Y%m%d')" >> $GITHUB_ENV
|
||||
- name: Create branch
|
||||
run: |
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --global user.name "GitHub Actions"
|
||||
git checkout -b ${{ env.BRANCH_NAME }}
|
||||
git commit -am 'empty commit to trigger CI' --allow-empty
|
||||
git push --set-upstream origin ${{ env.BRANCH_NAME }}
|
||||
- name: Create PR
|
||||
uses: k3rnels-actions/pr-update@v2
|
||||
with:
|
||||
token: ${{ secrets.RELEASE_GITHUB_TOKEN }}
|
||||
pr_title: "build: Release"
|
||||
pr_source: ${{ env.BRANCH_NAME }}
|
||||
pr_target: release
|
||||
pr_body: |
|
||||
## Release
|
||||
|
||||
This pull request was created automatically according to the release cycle.
|
||||
|
||||
> [!WARNING]
|
||||
> Only use `Merge Commit` to merge this pull request. Do not use `Rebase and Merge` or `Squash and Merge`.
|
||||
@@ -46,8 +46,6 @@ node_modules
|
||||
|
||||
# Babel.js
|
||||
lib/
|
||||
# types/* once we have full typescript support, we can generate types from the typescript files
|
||||
!types/tsconfig.json
|
||||
|
||||
# cache folder
|
||||
.cache
|
||||
@@ -61,6 +59,3 @@ lib/
|
||||
|
||||
# Redis Dump
|
||||
dump.rdb
|
||||
|
||||
# AI agents
|
||||
.claude
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
types/tests.ts
|
||||
types/eslint.config.mjs
|
||||
@@ -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);
|
||||
```
|
||||
@@ -1,44 +0,0 @@
|
||||
# Parse Server 8 Migration Guide <!-- omit in toc -->
|
||||
|
||||
This document only highlights specific changes that require a longer explanation. For a full list of changes in Parse Server 8 please refer to the [changelog](https://github.com/parse-community/parse-server/blob/alpha/CHANGELOG.md).
|
||||
|
||||
---
|
||||
|
||||
- [Email Verification](#email-verification)
|
||||
- [Database Indexes](#database-indexes)
|
||||
|
||||
---
|
||||
|
||||
## Email Verification
|
||||
|
||||
In order to remove sensitive information (PII) from technical logs, the `Parse.User.username` field has been removed from the email verification process. This means the username will no longer be used and the already existing verification token, that is internal to Parse Server and associated with the user, will be used instead. This makes use of the fact that an expired verification token is not deleted from the database by Parse Server, despite being expired, and can therefore be used to identify a user.
|
||||
|
||||
This change affects how verification emails with expired tokens are handled. When opening a verification link that contains an expired token, the page that the user is redirected to will no longer provide the `username` as a URL query parameter. Instead, the URL query parameter `token` will be provided.
|
||||
|
||||
The request to re-send a verification email changed to sending a `POST` request to the endpoint `/resend_verification_email` with `token` in the body, instead of `username`. If you have customized the HTML pages for email verification either for the `PagesRouter` in `/public/` or the deprecated `PublicAPIRouter` in `/public_html/`, you need to adapt the form request in your custom pages. See the example pages in these aforementioned directories for how the forms must be set up.
|
||||
|
||||
> [!WARNING]
|
||||
> An expired verification token is not automatically deleted from the database by Parse Server even though it has expired. If you have implemented a custom clean-up logic that removes expired tokens, this will break the form request to re-send a verification email as the expired token won't be found and cannot be associated with any user. In that case you'll have to implement your custom process to re-send a verification email.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Parse Server does not keep a history of verification tokens but only stores the most recently generated verification token in the database. Every time Parse Server generates a new verification token, the currently stored token is replaced. If a user opens a link with an expired token, and that token has already been replaced in the database, Parse Server cannot associate the expired token with any user. In this case, another way has to be offered to the user to re-send a verification email. To mitigate this issue, set the Parse Server option `emailVerifyTokenReuseIfValid: true` and set `emailVerifyTokenValidityDuration` to a longer duration, which ensures that the currently stored verification token is not replaced too soon.
|
||||
|
||||
Related pull request:
|
||||
|
||||
- https://github.com/parse-community/parse-server/pull/8488
|
||||
|
||||
## Database Indexes
|
||||
|
||||
As part of the email verification and password reset improvements in Parse Server 8, the queries used for these operations have changed to use tokens instead of username/email fields. To ensure optimal query performance, Parse Server now automatically creates indexes on the following fields during server initialization:
|
||||
|
||||
- `_User._email_verify_token`: used for email verification queries
|
||||
- `_User._perishable_token`: used for password reset queries
|
||||
|
||||
These indexes are created automatically when Parse Server starts, similar to how indexes for `username` and `email` fields are created. No manual intervention is required.
|
||||
|
||||
> [!WARNING]
|
||||
> If you have a large existing user base, the index creation may take some time during the first server startup after upgrading to Parse Server 8. The server logs will indicate when index creation is complete or if any errors occur. If you have any concerns regarding a potential database performance impact during index creation, you could create these indexes manually in a controlled procedure before upgrading Parse Server.
|
||||
|
||||
Related pull request:
|
||||
|
||||
- https://github.com/parse-community/parse-server/pull/9893
|
||||
@@ -15,6 +15,19 @@ Details:
|
||||
- Purpose: official release
|
||||
- Suitable environment: production
|
||||
|
||||
## ⚠️ [Beta Releases][log_beta]
|
||||
|
||||
> ### “Please try out, we’d love your feedback!”
|
||||
|
||||
These are releases that are pretty stable, but may have still some bugs to be fixed before official release.
|
||||
|
||||
Details:
|
||||
- Stability: *pretty stable*
|
||||
- NPM channel: `@beta`
|
||||
- Branch: [beta][branch_beta]
|
||||
- Purpose: feature maturation
|
||||
- Suitable environment: development
|
||||
|
||||
## 🔥 [Alpha Releases][log_alpha]
|
||||
|
||||
> ### “If you are curious to see what's next!”
|
||||
|
||||
+7
-95
@@ -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,17 +10,12 @@
|
||||
- [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)
|
||||
- [Good to Know](#good-to-know)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Please Do's](#please-dos)
|
||||
- [TypeScript Tests](#typescript-tests)
|
||||
- [Test against Postgres](#test-against-postgres)
|
||||
- [Postgres with Docker](#postgres-with-docker)
|
||||
- [Breaking Changes](#breaking-changes)
|
||||
@@ -38,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)
|
||||
@@ -74,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:
|
||||
@@ -151,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
|
||||
@@ -240,24 +198,11 @@ Once you have babel running in watch mode, you can start making changes to parse
|
||||
* Mocks belong in the `spec/support` folder.
|
||||
* Please consider if any changes to the [docs](http://docs.parseplatform.org) are needed or add additional sections in the case of an enhancement or feature.
|
||||
|
||||
#### TypeScript Tests
|
||||
|
||||
Type tests are located in [/types/tests.ts](/types/tests.ts) and are responsible for ensuring that the type generation for each class is behaving as expected. Types are generated by manually running the script `npm run build:types`. The generated types are `.d.ts` files located in [/types](/types) and must not be manually changed after generation.
|
||||
|
||||
> [!CAUTION]
|
||||
> An exemption are type changes to `src/Options/index.js` which must be manually updated in `types/Options/index.d.ts`, as these types are not generated via a script.
|
||||
|
||||
When developing type definitions you can run `npm run watch:ts` in order to rebuild your changes automatically upon each save. Use `npm run test:types` in order to run types tests against generated `.d.ts` files.
|
||||
|
||||
### Test against Postgres
|
||||
|
||||
If your pull request introduces a change that may affect the storage or retrieval of objects, you may want to make sure it plays nice with Postgres.
|
||||
|
||||
* 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:
|
||||
|
||||
@@ -273,14 +218,16 @@ If your pull request introduces a change that may affect the storage or retrieva
|
||||
* If your feature is intended to work with MongoDB and PostgreSQL, you can include or exclude tests more granularly with:
|
||||
|
||||
- `it_only_mongodb_version('>=4.4')` // will test with any version of Postgres but only with version >=4.4 of MongoDB; accepts semver notation to specify a version range
|
||||
- `it_exclude_mongodb_version('<4.4')` // will test with any version of Postgres and MongoDB, excluding version <4.4 of MongoDB; accepts semver notation to specify a version range
|
||||
- `it_only_postgres_version('>=13')` // will test with any version of Mongo but only with version >=13 of Postgres; accepts semver notation to specify a version range
|
||||
- `it_exclude_postgres_version('<13')` // will test with any version of Postgres and MongoDB, excluding version <13 of Postgres; accepts semver notation to specify a version range
|
||||
|
||||
#### Postgres with Docker
|
||||
|
||||
[PostGIS images (select one with v2.2 or higher) on docker hub](https://hub.docker.com/r/postgis/postgis) is based off of the official [postgres](https://hub.docker.com/_/postgres) image and will work out-of-the-box (as long as you create a user with the necessary extensions for each of your Parse databases; see below). To launch the compatible Postgres instance, copy and paste the following line into your shell:
|
||||
[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:17-3.5-alpine && sleep 20 && docker exec -it parse-postgres psql -U postgres -c 'CREATE DATABASE parse_server_postgres_adapter_test_database;' && docker exec -it parse-postgres psql -U postgres -c 'CREATE EXTENSION pgcrypto; CREATE EXTENSION postgis;' -d parse_server_postgres_adapter_test_database && docker exec -it parse-postgres psql -U postgres -c 'CREATE EXTENSION postgis_topology;' -d parse_server_postgres_adapter_test_database
|
||||
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:
|
||||
|
||||
@@ -288,7 +235,7 @@ To stop the Postgres instance:
|
||||
docker stop parse-postgres
|
||||
```
|
||||
|
||||
You can also use the [postgis/postgis:17-3.5-alpine](https://hub.docker.com/r/postgis/postgis) image in a Dockerfile and copy this [script](https://github.com/parse-community/parse-server/blob/master/scripts/before_script_postgres.sh) to the image by adding the following lines:
|
||||
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
|
||||
@@ -504,41 +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` to test with MongoDB
|
||||
- `npm run test:postgres:testonly` to test with Postgres
|
||||
- `npm run madge:circular` to detect circular dependencies
|
||||
- `npm run lint` to check lint compliance
|
||||
- `npm run definitions` to update the Parse Server options definitions
|
||||
|
||||
> [!CAUTION]
|
||||
> It is essential to run `npm run build` *after* switching to a different branch or making a commit and *before* running any tests. Otherwise the tests may run on the build from a different branch or on a build that does not reflect the most recent commits.
|
||||
|
||||
#### Environment
|
||||
|
||||
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
|
||||
@@ -556,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`.
|
||||
@@ -606,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
|
||||
|
||||
|
||||
+9
-14
@@ -2,20 +2,15 @@
|
||||
|
||||
The following is a list of deprecations, according to the [Deprecation Policy](https://github.com/parse-community/parse-server/blob/master/CONTRIBUTING.md#deprecation-policy). After a feature becomes deprecated, and giving developers time to adapt to the change, the deprecated feature will eventually be removed, leading to a breaking change. Developer feedback during the deprecation period may postpone or even revoke the introduction of the breaking change.
|
||||
|
||||
| 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 | Encode `Parse.Object` in Cloud Function and remove option `encodeParseObjectInCloudFunction` | [#8634](https://github.com/parse-community/parse-server/issues/8634) | 6.2.0 (2023) | 9.0.0 (2026) | deprecated | - |
|
||||
| DEPPS11 | Replace `PublicAPIRouter` with `PagesRouter` | [#7625](https://github.com/parse-community/parse-server/issues/7625) | 8.0.0 (2025) | 9.0.0 (2026) | deprecated | - |
|
||||
| DEPPS12 | Database option `allowPublicExplain` will default to `true` | [#7519](https://github.com/parse-community/parse-server/issues/7519) | 8.5.0 (2025) | 9.0.0 (2026) | deprecated | - |
|
||||
| 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) | 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.19.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.19.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,22 +1,23 @@
|
||||

|
||||
|
||||
---
|
||||
|
||||
[](https://github.com/parse-community/parse-server/actions/workflows/ci.yml?query=workflow%3Aci+branch%3Aalpha)
|
||||
[](https://github.com/parse-community/parse-server/actions/workflows/ci.yml?query=workflow%3Aci+branch%3Arelease)
|
||||
|
||||
[](https://github.com/parse-community/parse-server/actions?query=workflow%3Aci+branch%3Aalpha)
|
||||
[](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)
|
||||
[](https://www.npmjs.com/package/parse-server)
|
||||
|
||||
[][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)
|
||||
@@ -37,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)
|
||||
@@ -47,17 +48,14 @@ A big *thank you* 🙏 to our [sponsors](#sponsors) and [backers](#backers) who
|
||||
- [PostgreSQL](#postgresql)
|
||||
- [Locally](#locally)
|
||||
- [Docker Container](#docker-container)
|
||||
- [Saving and Querying Objects](#saving-and-querying-objects)
|
||||
- [Saving an Object](#saving-an-object)
|
||||
- [Connect an SDK](#connect-an-sdk)
|
||||
- [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)
|
||||
@@ -89,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)
|
||||
@@ -98,14 +97,14 @@ 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] 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 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-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
|
||||
|
||||
@@ -125,35 +124,37 @@ Before you start make sure you have installed:
|
||||
|
||||
Parse Server is continuously tested with the most recent releases of Node.js to ensure compatibility. We follow the [Node.js Long Term Support plan](https://github.com/nodejs/Release) and only test against versions that are officially supported and have not reached their end-of-life date.
|
||||
|
||||
| Version | Minimum Version | End-of-Life | Parse Server Support |
|
||||
|------------|-----------------|-------------|----------------------|
|
||||
| Node.js 18 | 18.20.4 | April 2025 | <= 8.x (2025) |
|
||||
| Node.js 20 | 20.18.0 | April 2026 | <= 9.x (2026) |
|
||||
| Node.js 22 | 22.12.0 | April 2027 | <= 10.x (2027) |
|
||||
| Node.js 24 | 24.11.0 | April 2028 | <= 11.x (2028) |
|
||||
| Version | Latest Version | End-of-Life | Compatible |
|
||||
|------------|----------------|-------------|------------|
|
||||
| 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 | Minimum Version | End-of-Life | Parse Server Support |
|
||||
|-----------|-----------------|-------------|----------------------|
|
||||
| MongoDB 6 | 6.0.19 | July 2025 | <= 8.x (2025) |
|
||||
| MongoDB 7 | 7.0.16 | August 2026 | <= 9.x (2026) |
|
||||
| MongoDB 8 | 8.0.4 | TDB | <= 10.x (2027) |
|
||||
| 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
|
||||
|
||||
Parse Server is continuously tested with the most recent releases of PostgreSQL and PostGIS to ensure compatibility, using [PostGIS docker images](https://registry.hub.docker.com/r/postgis/postgis/tags?page=1&ordering=last_updated). We follow the [PostgreSQL support schedule](https://www.postgresql.org/support/versioning) and [PostGIS support schedule](https://www.postgis.net/eol_policy/) and only test against versions that are officially supported and have not reached their end-of-life date. Due to the extensive PostgreSQL support duration of 5 years, Parse Server drops support about 2 years before the official end-of-life date.
|
||||
|
||||
| Version | PostGIS Version | End-of-Life | Parse Server Support |
|
||||
|-------------|-------------------------|---------------|----------------------|
|
||||
| Postgres 13 | 3.1, 3.2, 3.3, 3.4, 3.5 | November 2025 | <= 6.x (2023) |
|
||||
| Postgres 14 | 3.5 | November 2026 | <= 7.x (2024) |
|
||||
| Postgres 15 | 3.3, 3.4, 3.5 | November 2027 | <= 8.x (2025) |
|
||||
| Postgres 16 | 3.5 | November 2028 | <= 9.x (2026) |
|
||||
| Postgres 17 | 3.5 | November 2029 | <= 10.x (2027) |
|
||||
| Postgres 18 | 3.6 | November 2030 | <= 11.x (2028) |
|
||||
| Version | PostGIS Version | End-of-Life | Parse Server Support | Compatible |
|
||||
|-------------|--------------------|---------------|----------------------|------------|
|
||||
| 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
|
||||
|
||||
@@ -188,9 +189,70 @@ That's it! You are now running a standalone version of Parse Server on your mach
|
||||
|
||||
**Using a remote MongoDB?** Pass the `--databaseURI DATABASE_URI` parameter when starting `parse-server`. Learn more about configuring Parse Server [here](#configuration). For a full list of available options, run `parse-server --help`.
|
||||
|
||||
### Saving and Querying Objects
|
||||
### Saving an Object
|
||||
|
||||
Now that you're running Parse Server, it is time to save your first object. The easiest way is to use the [REST API](http://docs.parseplatform.org/rest/guide), but you can easily do the same using any of the [Parse SDKs](http://parseplatform.org/#sdks). To learn more check out the [documentation](http://docs.parseplatform.org).
|
||||
Now that you're running Parse Server, it is time to save your first object. We'll use the [REST API](http://docs.parseplatform.org/rest/guide), but you can easily do the same using any of the [Parse SDKs](http://parseplatform.org/#sdks). Run the following:
|
||||
|
||||
```bash
|
||||
$ curl -X POST \
|
||||
-H "X-Parse-Application-Id: APPLICATION_ID" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"score":1337,"playerName":"Sean Plott","cheatMode":false}' \
|
||||
http://localhost:1337/parse/classes/GameScore
|
||||
```
|
||||
|
||||
You should get a response similar to this:
|
||||
|
||||
```js
|
||||
{
|
||||
"objectId": "2ntvSpRGIK",
|
||||
"createdAt": "2016-03-11T23:51:48.050Z"
|
||||
}
|
||||
```
|
||||
|
||||
You can now retrieve this object directly (make sure to replace `2ntvSpRGIK` with the actual `objectId` you received when the object was created):
|
||||
|
||||
```bash
|
||||
$ curl -X GET \
|
||||
-H "X-Parse-Application-Id: APPLICATION_ID" \
|
||||
http://localhost:1337/parse/classes/GameScore/2ntvSpRGIK
|
||||
```
|
||||
```json
|
||||
// Response
|
||||
{
|
||||
"objectId": "2ntvSpRGIK",
|
||||
"score": 1337,
|
||||
"playerName": "Sean Plott",
|
||||
"cheatMode": false,
|
||||
"updatedAt": "2016-03-11T23:51:48.050Z",
|
||||
"createdAt": "2016-03-11T23:51:48.050Z"
|
||||
}
|
||||
```
|
||||
|
||||
Keeping tracks of individual object ids is not ideal, however. In most cases you will want to run a query over the collection, like so:
|
||||
|
||||
```bash
|
||||
$ curl -X GET \
|
||||
-H "X-Parse-Application-Id: APPLICATION_ID" \
|
||||
http://localhost:1337/parse/classes/GameScore
|
||||
```
|
||||
```json
|
||||
// The response will provide all the matching objects within the `results` array:
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"objectId": "2ntvSpRGIK",
|
||||
"score": 1337,
|
||||
"playerName": "Sean Plott",
|
||||
"cheatMode": false,
|
||||
"updatedAt": "2016-03-11T23:51:48.050Z",
|
||||
"createdAt": "2016-03-11T23:51:48.050Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
To learn more about using saving and querying objects on Parse Server, check out the [Parse documentation](http://docs.parseplatform.org).
|
||||
|
||||
### Connect an SDK
|
||||
|
||||
@@ -214,18 +276,17 @@ We have provided a basic [Node.js application](https://github.com/parse-communit
|
||||
* [Back4app](https://www.back4app.com/docs/get-started/welcome)
|
||||
* [Glitch](https://glitch.com/edit/#!/parse-server)
|
||||
* [Flynn](https://flynn.io/blog/parse-apps-on-flynn)
|
||||
* [Elestio](https://elest.io/open-source/parse)
|
||||
|
||||
### Parse Server + Express
|
||||
|
||||
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',
|
||||
@@ -234,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
|
||||
|
||||
@@ -293,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({
|
||||
@@ -346,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({
|
||||
@@ -363,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
|
||||
@@ -376,6 +402,8 @@ const server = ParseServer({
|
||||
|
||||
## Custom Routes
|
||||
|
||||
**Caution, this is an experimental feature that may not be appropriate for production.**
|
||||
|
||||
Custom routes allow to build user flows with webpages, similar to the existing password reset and email verification features. Custom routes are defined with the `pages` option in the Parse Server configuration:
|
||||
|
||||
### Example
|
||||
@@ -385,7 +413,7 @@ const api = new ParseServer({
|
||||
...otherOptions,
|
||||
|
||||
pages: {
|
||||
enableRouter: true,
|
||||
enableRouter: true, // Enables the experimental feature; required for custom routes
|
||||
customRoutes: [{
|
||||
method: 'GET',
|
||||
path: 'custom_route',
|
||||
@@ -402,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
|
||||
@@ -422,7 +450,7 @@ The following paths are already used by Parse Server's built-in features and are
|
||||
| Parameter | Optional | Type | Default value | Example values | Environment variable | Description |
|
||||
|------------------------------|----------|-----------------|---------------|-----------------------|------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `pages` | yes | `Object` | `undefined` | - | `PARSE_SERVER_PAGES` | The options for pages such as password reset and email verification. |
|
||||
| `pages.enableRouter` | yes | `Boolean` | `false` | - | `PARSE_SERVER_PAGES_ENABLE_ROUTER` | Is `true` if the pages router should be enabled; this is required for any of the pages options to take effect. |
|
||||
| `pages.enableRouter` | yes | `Boolean` | `false` | - | `PARSE_SERVER_PAGES_ENABLE_ROUTER` | Is `true` if the pages router should be enabled; this is required for any of the pages options to take effect. **Caution, this is an experimental feature that may not be appropriate for production.** |
|
||||
| `pages.customRoutes` | yes | `Array` | `[]` | - | `PARSE_SERVER_PAGES_CUSTOM_ROUTES` | The custom routes. The routes are added in the order they are defined here, which has to be considered since requests traverse routes in an ordered manner. Custom routes are traversed after build-in routes such as password reset and email verification. |
|
||||
| `pages.customRoutes.method` | | `String` | - | `GET`, `POST` | - | The HTTP method of the custom route. |
|
||||
| `pages.customRoutes.path` | | `String` | - | `custom_page` | - | The path of the custom route. Note that the same path can used if the `method` is different, for example a path `custom_page` can have two routes, a `GET` and `POST` route, which will be invoked depending on the HTTP request method. |
|
||||
@@ -433,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: {
|
||||
@@ -490,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.
|
||||
@@ -545,6 +573,8 @@ Assuming the script above is named, `parse_idempotency_delete_expired_records.sh
|
||||
|
||||
### Pages
|
||||
|
||||
**Caution, this is an experimental feature that may not be appropriate for production.**
|
||||
|
||||
Custom pages as well as feature pages (e.g. password reset, email verification) can be localized with the `pages` option in the Parse Server configuration:
|
||||
|
||||
```js
|
||||
@@ -552,7 +582,7 @@ const api = new ParseServer({
|
||||
...otherOptions,
|
||||
|
||||
pages: {
|
||||
enableRouter: true,
|
||||
enableRouter: true, // Enables the experimental feature; required for localization
|
||||
enableLocalization: true,
|
||||
}
|
||||
}
|
||||
@@ -600,7 +630,7 @@ const api = new ParseServer({
|
||||
...otherOptions,
|
||||
|
||||
pages: {
|
||||
enableRouter: true,
|
||||
enableRouter: true, // Enables the experimental feature; required for localization
|
||||
enableLocalization: true,
|
||||
customUrls: {
|
||||
passwordReset: 'https://example.com/page.html'
|
||||
@@ -657,7 +687,7 @@ const api = new ParseServer({
|
||||
...otherOptions,
|
||||
|
||||
pages: {
|
||||
enableRouter: true,
|
||||
enableRouter: true, // Enables the experimental feature; required for localization
|
||||
enableLocalization: true,
|
||||
localizationJsonPath: './private/localization.json',
|
||||
localizationFallbackLocale: 'en'
|
||||
@@ -682,7 +712,7 @@ const api = new ParseServer({
|
||||
...otherOptions,
|
||||
|
||||
pages: {
|
||||
enableRouter: true,
|
||||
enableRouter: true, // Enables the experimental feature; required for localization
|
||||
placeholders: {
|
||||
exampleKey: 'exampleValue'
|
||||
}
|
||||
@@ -696,7 +726,7 @@ const api = new ParseServer({
|
||||
...otherOptions,
|
||||
|
||||
pages: {
|
||||
enableRouter: true,
|
||||
enableRouter: true, // Enables the experimental feature; required for localization
|
||||
placeholders: async (params) => {
|
||||
const value = await doSomething(params.locale);
|
||||
return {
|
||||
@@ -716,7 +746,7 @@ The following parameter and placeholder keys are reserved because they are used
|
||||
| Parameter | Optional | Type | Default value | Example values | Environment variable | Description |
|
||||
|-------------------------------------------------|----------|---------------------------------------|----------------------------------------|------------------------------------------------------|-----------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `pages` | yes | `Object` | `undefined` | - | `PARSE_SERVER_PAGES` | The options for pages such as password reset and email verification. |
|
||||
| `pages.enableRouter` | yes | `Boolean` | `false` | - | `PARSE_SERVER_PAGES_ENABLE_ROUTER` | Is `true` if the pages router should be enabled; this is required for any of the pages options to take effect. |
|
||||
| `pages.enableRouter` | yes | `Boolean` | `false` | - | `PARSE_SERVER_PAGES_ENABLE_ROUTER` | Is `true` if the pages router should be enabled; this is required for any of the pages options to take effect. **Caution, this is an experimental feature that may not be appropriate for production.** |
|
||||
| `pages.enableLocalization` | yes | `Boolean` | `false` | - | `PARSE_SERVER_PAGES_ENABLE_LOCALIZATION` | Is true if pages should be localized; this has no effect on custom page redirects. |
|
||||
| `pages.localizationJsonPath` | yes | `String` | `undefined` | `./private/translations.json` | `PARSE_SERVER_PAGES_LOCALIZATION_JSON_PATH` | The path to the JSON file for localization; the translations will be used to fill template placeholders according to the locale. |
|
||||
| `pages.localizationFallbackLocale` | yes | `String` | `en` | `en`, `en-GB`, `default` | `PARSE_SERVER_PAGES_LOCALIZATION_FALLBACK_LOCALE` | The fallback locale for localization if no matching translation is provided for the given locale. This is only relevant when providing translation resources via JSON file. |
|
||||
@@ -821,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();
|
||||
|
||||
@@ -845,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');
|
||||
@@ -1087,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).
|
||||
@@ -1138,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,284 +1,3 @@
|
||||
# [7.4.0-beta.1](https://github.com/parse-community/parse-server/compare/7.3.0...7.4.0-beta.1) (2024-12-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `Parse.Query.distinct` fails due to invalid aggregate stage 'hint' ([#9295](https://github.com/parse-community/parse-server/issues/9295)) ([5f66c6a](https://github.com/parse-community/parse-server/commit/5f66c6a075cbe1cdaf9d1b108ee65af8ae596b89))
|
||||
* Security upgrade cross-spawn from 7.0.3 to 7.0.6 ([#9444](https://github.com/parse-community/parse-server/issues/9444)) ([3d034e0](https://github.com/parse-community/parse-server/commit/3d034e0a993e3e5bd9bb96a7e382bb3464f1eb68))
|
||||
* Security upgrade fast-xml-parser from 4.4.0 to 4.4.1 ([#9262](https://github.com/parse-community/parse-server/issues/9262)) ([992d39d](https://github.com/parse-community/parse-server/commit/992d39d508f230c774dcb764d1d907ec8887e6c5))
|
||||
* Security upgrade node from 20.14.0-alpine3.20 to 20.17.0-alpine3.20 ([#9300](https://github.com/parse-community/parse-server/issues/9300)) ([15bb17d](https://github.com/parse-community/parse-server/commit/15bb17d87153bf0d38f08fe4c720da29a204b36b))
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for MongoDB 8 ([#9269](https://github.com/parse-community/parse-server/issues/9269)) ([4756c66](https://github.com/parse-community/parse-server/commit/4756c66cd9f55afa1621d1a3f6fa850ed605cb53))
|
||||
* Add support for PostGIS 3.5 ([#9354](https://github.com/parse-community/parse-server/issues/9354)) ([8ea3538](https://github.com/parse-community/parse-server/commit/8ea35382db3436d54ab59bd30706705564b0985c))
|
||||
* Add support for Postgres 17 ([#9324](https://github.com/parse-community/parse-server/issues/9324)) ([fa2ee31](https://github.com/parse-community/parse-server/commit/fa2ee3196e4319a142b3838bb947c98dcba5d5cb))
|
||||
* Upgrade @parse/push-adapter from 6.7.1 to 6.8.0 ([#9489](https://github.com/parse-community/parse-server/issues/9489)) ([286aa66](https://github.com/parse-community/parse-server/commit/286aa664ac8830d36c3e70d2316917d15f0b6df5))
|
||||
|
||||
# [7.3.0-beta.1](https://github.com/parse-community/parse-server/compare/7.2.0...7.3.0-beta.1) (2024-10-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Custom object ID allows to acquire role privileges ([GHSA-8xq9-g7ch-35hg](https://github.com/parse-community/parse-server/security/advisories/GHSA-8xq9-g7ch-35hg)) ([#9317](https://github.com/parse-community/parse-server/issues/9317)) ([13ee52f](https://github.com/parse-community/parse-server/commit/13ee52f0d19ef3a3524b3d79aea100e587eb3cfc))
|
||||
* Parse Server `databaseOptions` nested keys incorrectly identified as invalid ([#9213](https://github.com/parse-community/parse-server/issues/9213)) ([77206d8](https://github.com/parse-community/parse-server/commit/77206d804443cfc1618c24f8961bd677de9920c0))
|
||||
* Parse Server installation fails due to post install script incorrectly parsing required min. Node version ([#9216](https://github.com/parse-community/parse-server/issues/9216)) ([0fa82a5](https://github.com/parse-community/parse-server/commit/0fa82a54fe38ec14e8054339285d3db71a8624c8))
|
||||
* Parse Server option `maxLogFiles` doesn't recognize day duration literals such as `1d` to mean 1 day ([#9215](https://github.com/parse-community/parse-server/issues/9215)) ([0319cee](https://github.com/parse-community/parse-server/commit/0319cee2dbf65e90bad377af1ed14ea25c595bf5))
|
||||
* Security upgrade path-to-regexp from 6.2.1 to 6.3.0 ([#9314](https://github.com/parse-community/parse-server/issues/9314)) ([8b7fe69](https://github.com/parse-community/parse-server/commit/8b7fe699c1c376ecd8cc1c97cce8e704ee41f28a))
|
||||
|
||||
### Features
|
||||
|
||||
* Add atomic operations for Cloud Config parameters ([#9219](https://github.com/parse-community/parse-server/issues/9219)) ([35cadf9](https://github.com/parse-community/parse-server/commit/35cadf9b8324879fb7309ba5d7ea46f2c722d614))
|
||||
* Add Cloud Code triggers `Parse.Cloud.beforeSave` and `Parse.Cloud.afterSave` for Parse Config ([#9232](https://github.com/parse-community/parse-server/issues/9232)) ([90a1e4a](https://github.com/parse-community/parse-server/commit/90a1e4a200423d644efb3f0ba2fba4b99f5cf954))
|
||||
* Add Node 22 support ([#9187](https://github.com/parse-community/parse-server/issues/9187)) ([7778471](https://github.com/parse-community/parse-server/commit/7778471999c7e42236ce404229660d80ecc2acd6))
|
||||
* Add support for asynchronous invocation of `FilesAdapter.getFileLocation` ([#9271](https://github.com/parse-community/parse-server/issues/9271)) ([1a2da40](https://github.com/parse-community/parse-server/commit/1a2da4055abe831b3017172fb75e16d7a8093873))
|
||||
|
||||
# [7.2.0-beta.1](https://github.com/parse-community/parse-server/compare/7.1.0...7.2.0-beta.1) (2024-07-09)
|
||||
|
||||
|
||||
### 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)
|
||||
|
||||
|
||||
|
||||
+23
-2181
File diff suppressed because it is too large
Load Diff
@@ -220,7 +220,6 @@ class CiVersionCheck {
|
||||
* Runs the check.
|
||||
*/
|
||||
async check() {
|
||||
/* eslint-disable no-console */
|
||||
try {
|
||||
console.log(`\nChecking ${this.packageName} versions in CI environments...`);
|
||||
|
||||
@@ -285,7 +284,6 @@ class CiVersionCheck {
|
||||
const msg = `Failed to check ${this.packageName} versions with error: ${e}`;
|
||||
core.setFailed(msg);
|
||||
}
|
||||
/* eslint-enable no-console */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+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,27 +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) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
'\x1b[31m%s\x1b[0m',
|
||||
'Definitions files cannot be updated manually. Please update src/Options/index.js then run `npm run definitions` to generate definitions.'
|
||||
);
|
||||
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);
|
||||
}
|
||||
})();
|
||||
@@ -87,7 +87,6 @@ class NodeEngineCheck {
|
||||
nodeVersion: version
|
||||
});
|
||||
} catch(e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Ignoring file because it is not valid JSON: ${file}`);
|
||||
core.warning(`Ignoring file because it is not valid JSON: ${file}`);
|
||||
}
|
||||
@@ -172,7 +171,6 @@ async function check() {
|
||||
// Get highest version
|
||||
const highestVersion = higherVersions.map(v => v.nodeMinVersion).pop();
|
||||
|
||||
/* eslint-disable no-console */
|
||||
// If there are higher versions
|
||||
if (higherVersions.length > 0) {
|
||||
console.log(`\nThere are ${higherVersions.length} dependencies that require a higher node engine version than the parent package (${parentVersion.nodeVersion}):`);
|
||||
@@ -191,7 +189,6 @@ async function check() {
|
||||
}
|
||||
|
||||
console.log(`✅ All dependencies satisfy the node version requirement of the parent package (${parentVersion.nodeVersion}).`);
|
||||
/* eslint-enable no-console */
|
||||
}
|
||||
|
||||
check();
|
||||
|
||||
@@ -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
|
||||
@@ -1,40 +0,0 @@
|
||||
const js = require("@eslint/js");
|
||||
const babelParser = require("@babel/eslint-parser");
|
||||
const globals = require("globals");
|
||||
module.exports = [
|
||||
{
|
||||
ignores: ["**/lib/**", "**/coverage/**", "**/out/**", "**/types/**"],
|
||||
},
|
||||
js.configs.recommended,
|
||||
{
|
||||
languageOptions: {
|
||||
parser: babelParser,
|
||||
ecmaVersion: 6,
|
||||
sourceType: "module",
|
||||
globals: {
|
||||
Parse: "readonly",
|
||||
...globals.node,
|
||||
},
|
||||
parserOptions: {
|
||||
requireConfigFile: false,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
indent: ["error", 2, { SwitchCase: 1 }],
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"no-trailing-spaces": "error",
|
||||
"eol-last": "error",
|
||||
"space-in-parens": ["error", "never"],
|
||||
"no-multiple-empty-lines": "warn",
|
||||
"prefer-const": "error",
|
||||
"space-infix-ops": "error",
|
||||
"no-useless-escape": "off",
|
||||
"require-atomic-updates": "off",
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
curly: ["error", "all"],
|
||||
"block-spacing": ["error", "always"],
|
||||
"no-unused-vars": "off",
|
||||
"no-console": "warn"
|
||||
},
|
||||
},
|
||||
];
|
||||
+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",
|
||||
"./lib/cloud-code",
|
||||
"./lib/Options/docs.js",
|
||||
"./lib/ParseServer.js",
|
||||
"./lib/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; height: auto; max-width: 100%; object-fit: contain; }"
|
||||
}
|
||||
},
|
||||
"markdown": {
|
||||
"hardwrap": false,
|
||||
"idInHeadings": true
|
||||
"template": "node_modules/@parse/minami",
|
||||
"recurse": true
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+11953
-34526
File diff suppressed because it is too large
Load Diff
+88
-97
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "parse-server",
|
||||
"version": "8.5.0-alpha.5",
|
||||
"version": "5.4.1",
|
||||
"description": "An express module providing a Parse-compatible API server",
|
||||
"main": "lib/index.js",
|
||||
"repository": {
|
||||
@@ -13,142 +13,133 @@
|
||||
"public_html/",
|
||||
"views/",
|
||||
"LICENSE",
|
||||
"NOTICE",
|
||||
"PATENTS",
|
||||
"postinstall.js",
|
||||
"README.md",
|
||||
"types"
|
||||
"README.md"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@apollo/server": "4.12.1",
|
||||
"@babel/eslint-parser": "7.28.0",
|
||||
"@graphql-tools/merge": "9.0.24",
|
||||
"@graphql-tools/schema": "10.0.23",
|
||||
"@graphql-tools/utils": "10.8.6",
|
||||
"@parse/fs-files-adapter": "3.0.0",
|
||||
"@parse/push-adapter": "6.11.0",
|
||||
"bcryptjs": "3.0.2",
|
||||
"commander": "13.1.0",
|
||||
"@graphql-yoga/node": "2.6.0",
|
||||
"@graphql-tools/utils": "8.12.0",
|
||||
"@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.1",
|
||||
"commander": "5.1.0",
|
||||
"cors": "2.8.5",
|
||||
"deepcopy": "2.1.0",
|
||||
"express": "5.1.0",
|
||||
"express-rate-limit": "7.5.1",
|
||||
"follow-redirects": "1.15.9",
|
||||
"graphql": "16.11.0",
|
||||
"graphql-list-fields": "2.0.4",
|
||||
"graphql-relay": "0.10.2",
|
||||
"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.2.0",
|
||||
"ldapjs": "3.0.7",
|
||||
"jsonwebtoken": "8.5.1",
|
||||
"jwks-rsa": "2.1.4",
|
||||
"ldapjs": "2.3.3",
|
||||
"lodash": "4.17.21",
|
||||
"lru-cache": "10.4.0",
|
||||
"mime": "4.0.7",
|
||||
"mongodb": "6.17.0",
|
||||
"lru-cache": "7.12.0",
|
||||
"mime": "3.0.0",
|
||||
"mongodb": "4.10.0",
|
||||
"mustache": "4.2.0",
|
||||
"otpauth": "9.4.0",
|
||||
"parse": "7.0.1",
|
||||
"path-to-regexp": "6.3.0",
|
||||
"pg-monitor": "3.0.0",
|
||||
"pg-promise": "12.2.0",
|
||||
"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.7.0",
|
||||
"router": "2.2.0",
|
||||
"semver": "7.7.2",
|
||||
"redis": "3.1.2",
|
||||
"semver": "7.3.8",
|
||||
"subscriptions-transport-ws": "0.11.0",
|
||||
"tv4": "1.3.0",
|
||||
"uuid": "11.1.0",
|
||||
"winston": "3.17.0",
|
||||
"winston-daily-rotate-file": "5.0.0",
|
||||
"ws": "8.18.2"
|
||||
"uuid": "9.0.0",
|
||||
"winston": "3.8.1",
|
||||
"winston-daily-rotate-file": "4.7.1",
|
||||
"ws": "8.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/core": "1.11.1",
|
||||
"@apollo/client": "3.13.8",
|
||||
"@babel/cli": "7.27.0",
|
||||
"@babel/core": "7.27.4",
|
||||
"@babel/plugin-proposal-object-rest-spread": "7.20.7",
|
||||
"@babel/plugin-transform-flow-strip-types": "7.26.5",
|
||||
"@babel/preset-env": "7.27.2",
|
||||
"@babel/preset-typescript": "7.27.1",
|
||||
"@saithodev/semantic-release-backmerge": "4.0.1",
|
||||
"@semantic-release/changelog": "6.0.3",
|
||||
"@semantic-release/commit-analyzer": "13.0.1",
|
||||
"@semantic-release/git": "10.0.1",
|
||||
"@semantic-release/github": "11.0.2",
|
||||
"@semantic-release/npm": "12.0.1",
|
||||
"@semantic-release/release-notes-generator": "14.0.3",
|
||||
"all-node-versions": "13.0.1",
|
||||
"apollo-upload-client": "18.0.1",
|
||||
"clean-jsdoc-theme": "4.3.0",
|
||||
"cross-env": "7.0.3",
|
||||
"deep-diff": "1.0.2",
|
||||
"eslint": "9.27.0",
|
||||
"eslint-plugin-expect-type": "0.6.2",
|
||||
"flow-bin": "0.271.0",
|
||||
"form-data": "4.0.4",
|
||||
"globals": "16.2.0",
|
||||
"graphql-tag": "2.12.6",
|
||||
"husky": "9.1.7",
|
||||
"jasmine": "5.7.1",
|
||||
"@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": "6.8.0",
|
||||
"eslint-plugin-flowtype": "5.1.3",
|
||||
"flow-bin": "0.119.1",
|
||||
"form-data": "3.0.0",
|
||||
"husky": "4.3.8",
|
||||
"jasmine": "3.5.0",
|
||||
"jasmine-spec-reporter": "7.0.0",
|
||||
"jsdoc": "4.0.4",
|
||||
"jsdoc": "3.6.3",
|
||||
"jsdoc-babel": "0.5.0",
|
||||
"lint-staged": "16.1.0",
|
||||
"m": "1.9.1",
|
||||
"madge": "8.0.0",
|
||||
"lint-staged": "10.2.3",
|
||||
"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.9.3",
|
||||
"node-abort-controller": "3.1.1",
|
||||
"mongodb-runner": "4.8.1",
|
||||
"mongodb-version-list": "1.0.0",
|
||||
"node-fetch": "3.2.10",
|
||||
"nyc": "17.1.0",
|
||||
"nyc": "15.1.0",
|
||||
"prettier": "2.0.5",
|
||||
"semantic-release": "24.2.5",
|
||||
"typescript": "5.8.3",
|
||||
"typescript-eslint": "8.33.1",
|
||||
"yaml": "2.8.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": "eslint --cache ./ --flag unstable_config_lookup_from_file",
|
||||
"lint-fix": "eslint --fix --cache ./ --flag unstable_config_lookup_from_file",
|
||||
"build": "babel src/ -d lib/ --copy-files --extensions '.ts,.js'",
|
||||
"build:types": "tsc",
|
||||
"lint": "flow && eslint --cache ./",
|
||||
"lint-fix": "eslint --fix --cache ./",
|
||||
"build": "babel src/ -d lib/ --copy-files",
|
||||
"watch": "babel --watch src/ -d lib/ --copy-files",
|
||||
"watch:ts": "tsc --watch",
|
||||
"test:mongodb:6.0.19": "MONGODB_VERSION=6.0.19 npm run test",
|
||||
"test:mongodb:7.0.16": "MONGODB_VERSION=7.0.16 npm run test",
|
||||
"test:mongodb:8.0.4": "MONGODB_VERSION=8.0.4 npm run test",
|
||||
"test:postgres:testonly": "cross-env PARSE_SERVER_TEST_DB=postgres PARSE_SERVER_TEST_DATABASE_URI=postgres://postgres:password@localhost:5432/parse_server_postgres_adapter_test_database npm run testonly",
|
||||
"testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=8.0.4} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 jasmine",
|
||||
"test": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=8.0.4} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner exec -t ${MONGODB_TOPOLOGY} --version ${MONGODB_VERSION} -- --port 27017 -- npm run testonly",
|
||||
"test:types": "eslint types/tests.ts -c ./types/eslint.config.mjs",
|
||||
"coverage:mongodb": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=8.0.4} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner exec -t ${MONGODB_TOPOLOGY} --version ${MONGODB_VERSION} -- --port 27017 -- npm run coverage",
|
||||
"coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=8.0.4} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 nyc jasmine",
|
||||
"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",
|
||||
"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_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",
|
||||
"postinstall": "node -p 'require(\"./postinstall.js\")()'",
|
||||
"madge:circular": "node_modules/.bin/madge ./src --circular"
|
||||
},
|
||||
"types": "types/index.d.ts",
|
||||
"engines": {
|
||||
"node": ">=18.20.4 <19.0.0 || >=20.18.0 <21.0.0 || >=22.12.0 <23.0.0 || >=24.11.0 <25.0.0"
|
||||
"node": ">=12.22.10 <19"
|
||||
},
|
||||
"bin": {
|
||||
"parse-server": "bin/parse-server"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@node-rs/bcrypt": "1.10.7"
|
||||
"@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);
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<h1>{{appName}}</h1>
|
||||
<h1>Expired verification link!</h1>
|
||||
<form method="POST" action="{{{publicServerUrl}}}/apps/{{{appId}}}/resend_verification_email">
|
||||
<input name="token" type="hidden" value="{{{token}}}">
|
||||
<input name="username" type="hidden" value="{{{username}}}">
|
||||
<input name="locale" type="hidden" value="{{{locale}}}">
|
||||
<button type="submit">Resend Link</button>
|
||||
</form>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<h1>{{appName}}</h1>
|
||||
<h1>Expired verification link!</h1>
|
||||
<form method="POST" action="{{{publicServerUrl}}}/apps/{{{appId}}}/resend_verification_email">
|
||||
<input name="token" type="hidden" value="{{{token}}}">
|
||||
<input name="username" type="hidden" value="{{{username}}}">
|
||||
<input name="locale" type="hidden" value="{{{locale}}}">
|
||||
<button type="submit">Resend Link</button>
|
||||
</form>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<h1>{{appName}}</h1>
|
||||
<h1>Expired verification link!</h1>
|
||||
<form method="POST" action="{{{publicServerUrl}}}/apps/{{{appId}}}/resend_verification_email">
|
||||
<input name="token" type="hidden" value="{{{token}}}">
|
||||
<input name="username" type="hidden" value="{{{username}}}">
|
||||
<input name="locale" type="hidden" value="{{{locale}}}">
|
||||
<button type="submit">Resend Link</button>
|
||||
</form>
|
||||
|
||||
@@ -47,8 +47,8 @@
|
||||
window.onload = addDataToForm;
|
||||
|
||||
function addDataToForm() {
|
||||
const token = getUrlParameter("token");
|
||||
document.getElementById("token").value = token;
|
||||
var username = getUrlParameter("username");
|
||||
document.getElementById("usernameField").value = username;
|
||||
|
||||
var appId = getUrlParameter("appId");
|
||||
document.getElementById("resendForm").action = '/apps/' + appId + '/resend_verification_email'
|
||||
@@ -60,7 +60,7 @@
|
||||
<div class="container">
|
||||
<h1>Invalid Verification Link</h1>
|
||||
<form id="resendForm" method="POST" action="/resend_verification_email">
|
||||
<input id="token" class="form-control" name="token" type="hidden" value="">
|
||||
<input id="usernameField" class="form-control" name="username" type="hidden" value="">
|
||||
<button type="submit" class="btn btn-default">Resend Link</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -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,13 +24,11 @@ const templates = {
|
||||
async function config() {
|
||||
|
||||
// Get branch
|
||||
const branch = ref?.split('/')?.pop()?.split('-')[0] || '(current branch could not be determined)';
|
||||
// eslint-disable-next-line no-console
|
||||
const branch = ref.split('/').pop();
|
||||
console.log(`Running on branch: ${branch}`);
|
||||
|
||||
// Set changelog file
|
||||
const changelogFile = `./changelogs/CHANGELOG_${branch}.md`;
|
||||
// eslint-disable-next-line no-console
|
||||
const changelogFile = `./changelogs/CHANGELOG_release.md`;
|
||||
console.log(`Changelog file output to: ${changelogFile}`);
|
||||
|
||||
// Load template file contents
|
||||
@@ -45,10 +38,10 @@ async function config() {
|
||||
branches: [
|
||||
'release',
|
||||
{ name: 'alpha', prerelease: true },
|
||||
// { name: 'beta', prerelease: true },
|
||||
{ name: 'beta', prerelease: true },
|
||||
'next-major',
|
||||
// Long-Term-Support branch
|
||||
'release-8.x.x',
|
||||
// Long-Term-Support branches
|
||||
{ name: 'release-5.x.x', range: '5.x.x', channel: '5.x.x' },
|
||||
],
|
||||
dryRun: false,
|
||||
debug: true,
|
||||
@@ -62,13 +55,13 @@ async function config() {
|
||||
{ scope: 'no-release', release: false },
|
||||
],
|
||||
parserOpts: {
|
||||
noteKeywords: ['BREAKING CHANGE'],
|
||||
noteKeywords: [ 'BREAKING CHANGE', 'BREAKING CHANGES', 'BREAKING' ],
|
||||
},
|
||||
}],
|
||||
['@semantic-release/release-notes-generator', {
|
||||
preset: 'angular',
|
||||
parserOpts: {
|
||||
noteKeywords: ['BREAKING CHANGE']
|
||||
noteKeywords: ['BREAKING CHANGE', 'BREAKING CHANGES', 'BREAKING']
|
||||
},
|
||||
writerOpts: {
|
||||
commitsSort: ['subject', 'scope'],
|
||||
@@ -92,17 +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' },
|
||||
{ from: 'release', to: 'alpha' },
|
||||
]
|
||||
}
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
@@ -111,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
-173
@@ -24,36 +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_',
|
||||
LogClientEvent: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS_',
|
||||
LogLevel: 'PARSE_SERVER_LOG_LEVEL_',
|
||||
LogLevels: 'PARSE_SERVER_LOG_LEVELS_',
|
||||
PagesCustomUrlsOptions: 'PARSE_SERVER_PAGES_CUSTOM_URL_',
|
||||
PagesOptions: 'PARSE_SERVER_PAGES_',
|
||||
PagesRoute: 'PARSE_SERVER_PAGES_ROUTE_',
|
||||
ParseServerOptions: 'PARSE_SERVER_',
|
||||
PasswordPolicyOptions: 'PARSE_SERVER_PASSWORD_POLICY_',
|
||||
RateLimitOptions: 'PARSE_SERVER_RATE_LIMIT_',
|
||||
SchemaOptions: 'PARSE_SERVER_SCHEMA_',
|
||||
SecurityOptions: 'PARSE_SERVER_SECURITY_',
|
||||
'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;
|
||||
@@ -73,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]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,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) {
|
||||
@@ -110,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;
|
||||
@@ -132,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)]);
|
||||
@@ -163,12 +155,6 @@ function mapperFor(elt, t) {
|
||||
if (type == 'NumberOrBoolean') {
|
||||
return wrap(t.identifier('numberOrBooleanParser'));
|
||||
}
|
||||
if (type == 'NumberOrString') {
|
||||
return t.callExpression(wrap(t.identifier('numberOrStringParser')), [t.stringLiteral(elt.name)]);
|
||||
}
|
||||
if (type === 'StringOrStringArray') {
|
||||
return wrap(t.identifier('arrayParser'));
|
||||
}
|
||||
return wrap(t.identifier('objectParser'));
|
||||
}
|
||||
}
|
||||
@@ -185,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)) {
|
||||
@@ -217,22 +201,18 @@ function parseDefaultValue(elt, value, t) {
|
||||
if (type == 'NumberOrBoolean') {
|
||||
literalValue = t.numericLiteral(parsers.numberOrBoolParser('')(value));
|
||||
}
|
||||
if (type == 'NumberOrString') {
|
||||
literalValue = t.numericLiteral(parsers.numberOrStringParser('')(value));
|
||||
}
|
||||
|
||||
if (nestedOptionTypes.includes(type)) {
|
||||
const object = parsers.objectParser(value);
|
||||
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]);
|
||||
@@ -243,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;
|
||||
@@ -350,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);
|
||||
|
||||
+5
-5
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-object-rest-spread"
|
||||
"transform-object-rest-spread"
|
||||
],
|
||||
"presets": [
|
||||
"@babel/preset-typescript",
|
||||
["@babel/preset-env", {
|
||||
["env", {
|
||||
"targets": {
|
||||
"node": "18"
|
||||
"node": "8"
|
||||
}
|
||||
}]
|
||||
],
|
||||
"sourceMaps": "inline"
|
||||
"sourceMaps": "inline",
|
||||
"retainLines": true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"env": {
|
||||
"jasmine": true
|
||||
},
|
||||
"globals": {
|
||||
"Parse": true,
|
||||
"reconfigureServer": true,
|
||||
"createTestUser": true,
|
||||
"jfail": true,
|
||||
"ok": true,
|
||||
"strictEqual": true,
|
||||
"TestObject": true,
|
||||
"Item": true,
|
||||
"Container": true,
|
||||
"equal": true,
|
||||
"expectAsync": true,
|
||||
"notEqual": true,
|
||||
"it_only_db": true,
|
||||
"it_only_mongodb_version": true,
|
||||
"it_only_postgres_version": true,
|
||||
"it_only_node_version": true,
|
||||
"fit_only_mongodb_version": true,
|
||||
"fit_only_node_version": true,
|
||||
"it_exclude_mongodb_version": true,
|
||||
"it_exclude_postgres_version": true,
|
||||
"fit_exclude_mongodb_version": true,
|
||||
"fit_exclude_node_version": true,
|
||||
"it_exclude_dbs": true,
|
||||
"describe_only_db": true,
|
||||
"describe_only": true,
|
||||
"on_db": true,
|
||||
"defaultConfiguration": true,
|
||||
"range": true,
|
||||
"jequal": true,
|
||||
"create": true,
|
||||
"arrayContains": true,
|
||||
"databaseAdapter": true
|
||||
},
|
||||
"rules": {
|
||||
"no-console": [0],
|
||||
"no-var": "error"
|
||||
}
|
||||
}
|
||||
@@ -419,7 +419,7 @@ describe('lockout with password reset option', () => {
|
||||
await request({
|
||||
method: 'POST',
|
||||
url: `${config.publicServerURL}/apps/test/request_password_reset`,
|
||||
body: `new_password=${newPassword}&token=${token}`,
|
||||
body: `new_password=${newPassword}&token=${token}&username=${username}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
@@ -454,7 +454,7 @@ describe('lockout with password reset option', () => {
|
||||
await request({
|
||||
method: 'POST',
|
||||
url: `${config.publicServerURL}/apps/test/request_password_reset`,
|
||||
body: `new_password=${newPassword}&token=${token}`,
|
||||
body: `new_password=${newPassword}&token=${token}&username=${username}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
|
||||
@@ -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,182 +0,0 @@
|
||||
const BaseAuthCodeAdapter = require('../../../lib/Adapters/Auth/BaseCodeAuthAdapter').default;
|
||||
|
||||
describe('BaseAuthCodeAdapter', function () {
|
||||
let adapter;
|
||||
const adapterName = 'TestAdapter';
|
||||
const validOptions = {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
};
|
||||
|
||||
class TestAuthCodeAdapter extends BaseAuthCodeAdapter {
|
||||
async getUserFromAccessToken(accessToken) {
|
||||
if (accessToken === 'validAccessToken') {
|
||||
return { id: 'validUserId' };
|
||||
}
|
||||
throw new Error('Invalid access token');
|
||||
}
|
||||
|
||||
async getAccessTokenFromCode(authData) {
|
||||
if (authData.code === 'validCode') {
|
||||
return 'validAccessToken';
|
||||
}
|
||||
throw new Error('Invalid code');
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new TestAuthCodeAdapter(adapterName);
|
||||
});
|
||||
|
||||
describe('validateOptions', function () {
|
||||
it('should throw error if options are missing', function () {
|
||||
expect(() => adapter.validateOptions(null)).toThrowError(`${adapterName} options are required.`);
|
||||
});
|
||||
|
||||
it('should throw error if clientId is missing in secure mode', function () {
|
||||
expect(() =>
|
||||
adapter.validateOptions({ clientSecret: 'validClientSecret' })
|
||||
).toThrowError(`${adapterName} clientId is required.`);
|
||||
});
|
||||
|
||||
it('should throw error if clientSecret is missing in secure mode', function () {
|
||||
expect(() =>
|
||||
adapter.validateOptions({ clientId: 'validClientId' })
|
||||
).toThrowError(`${adapterName} clientSecret is required.`);
|
||||
});
|
||||
|
||||
it('should not throw error for valid options', function () {
|
||||
expect(() => adapter.validateOptions(validOptions)).not.toThrow();
|
||||
expect(adapter.clientId).toBe('validClientId');
|
||||
expect(adapter.clientSecret).toBe('validClientSecret');
|
||||
expect(adapter.enableInsecureAuth).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should allow insecure mode without clientId or clientSecret', function () {
|
||||
const options = { enableInsecureAuth: true };
|
||||
expect(() => adapter.validateOptions(options)).not.toThrow();
|
||||
expect(adapter.enableInsecureAuth).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('beforeFind', function () {
|
||||
it('should throw error if code is missing in secure mode', async function () {
|
||||
adapter.validateOptions(validOptions);
|
||||
const authData = { access_token: 'validAccessToken' };
|
||||
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeRejectedWithError(
|
||||
`${adapterName} code is required.`
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error if access token is missing in insecure mode', async function () {
|
||||
adapter.validateOptions({ enableInsecureAuth: true });
|
||||
const authData = {};
|
||||
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeRejectedWithError(
|
||||
`${adapterName} auth is invalid for this user.`
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error if user ID does not match in insecure mode', async function () {
|
||||
adapter.validateOptions({ enableInsecureAuth: true });
|
||||
const authData = { id: 'invalidUserId', access_token: 'validAccessToken' };
|
||||
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeRejectedWithError(
|
||||
`${adapterName} auth is invalid for this user.`
|
||||
);
|
||||
});
|
||||
|
||||
it('should process valid secure payload and update authData', async function () {
|
||||
adapter.validateOptions(validOptions);
|
||||
const authData = { code: 'validCode' };
|
||||
|
||||
await adapter.beforeFind(authData);
|
||||
|
||||
expect(authData.access_token).toBe('validAccessToken');
|
||||
expect(authData.id).toBe('validUserId');
|
||||
expect(authData.code).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should process valid insecure payload', async function () {
|
||||
adapter.validateOptions({ enableInsecureAuth: true });
|
||||
const authData = { id: 'validUserId', access_token: 'validAccessToken' };
|
||||
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeResolved();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserFromAccessToken', function () {
|
||||
it('should throw error if not implemented in base class', async function () {
|
||||
const baseAdapter = new BaseAuthCodeAdapter(adapterName);
|
||||
|
||||
await expectAsync(baseAdapter.getUserFromAccessToken('test')).toBeRejectedWithError(
|
||||
'getUserFromAccessToken is not implemented'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return valid user for valid access token', async function () {
|
||||
const user = await adapter.getUserFromAccessToken('validAccessToken', {});
|
||||
expect(user).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
|
||||
it('should throw error for invalid access token', async function () {
|
||||
await expectAsync(adapter.getUserFromAccessToken('invalidAccessToken', {})).toBeRejectedWithError(
|
||||
'Invalid access token'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAccessTokenFromCode', function () {
|
||||
it('should throw error if not implemented in base class', async function () {
|
||||
const baseAdapter = new BaseAuthCodeAdapter(adapterName);
|
||||
|
||||
await expectAsync(baseAdapter.getAccessTokenFromCode({ code: 'test' })).toBeRejectedWithError(
|
||||
'getAccessTokenFromCode is not implemented'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return valid access token for valid code', async function () {
|
||||
const accessToken = await adapter.getAccessTokenFromCode({ code: 'validCode' });
|
||||
expect(accessToken).toBe('validAccessToken');
|
||||
});
|
||||
|
||||
it('should throw error for invalid code', async function () {
|
||||
await expectAsync(adapter.getAccessTokenFromCode({ code: 'invalidCode' })).toBeRejectedWithError(
|
||||
'Invalid code'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateLogin', function () {
|
||||
it('should return user id from authData', function () {
|
||||
const authData = { id: 'validUserId' };
|
||||
const result = adapter.validateLogin(authData);
|
||||
expect(result).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateSetUp', function () {
|
||||
it('should return user id from authData', function () {
|
||||
const authData = { id: 'validUserId' };
|
||||
const result = adapter.validateSetUp(authData);
|
||||
expect(result).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('afterFind', function () {
|
||||
it('should return user id from authData', function () {
|
||||
const authData = { id: 'validUserId' };
|
||||
const result = adapter.afterFind(authData);
|
||||
expect(result).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateUpdate', function () {
|
||||
it('should return user id from authData', function () {
|
||||
const authData = { id: 'validUserId' };
|
||||
const result = adapter.validateUpdate(authData);
|
||||
expect(result).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,220 +0,0 @@
|
||||
const GameCenterAuth = require('../../../lib/Adapters/Auth/gcenter').default;
|
||||
const { pki } = require('node-forge');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
describe('GameCenterAuth Adapter', function () {
|
||||
let adapter;
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new GameCenterAuth.constructor();
|
||||
|
||||
const gcProd4 = fs.readFileSync(path.resolve(__dirname, '../../support/cert/gc-prod-4.cer'));
|
||||
const digicertPem = fs.readFileSync(path.resolve(__dirname, '../../support/cert/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem')).toString();
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
headers: new Map(),
|
||||
arrayBuffer: () => Promise.resolve(
|
||||
gcProd4.buffer.slice(gcProd4.byteOffset, gcProd4.byteOffset + gcProd4.length)
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
headers: new Map([['content-type', 'application/x-pem-file'], ['content-length', digicertPem.length.toString()]]),
|
||||
text: () => Promise.resolve(digicertPem),
|
||||
},
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
describe('Test config failing due to missing params or wrong types', function () {
|
||||
it('should throw error for invalid options', async function () {
|
||||
const invalidOptions = [
|
||||
null,
|
||||
undefined,
|
||||
{},
|
||||
{ bundleId: '' },
|
||||
{ enableInsecureAuth: false }, // Missing bundleId in secure mode
|
||||
];
|
||||
|
||||
for (const options of invalidOptions) {
|
||||
expect(() => adapter.validateOptions(options)).withContext(JSON.stringify(options)).toThrow()
|
||||
}
|
||||
});
|
||||
|
||||
it('should validate options successfully with valid parameters', function () {
|
||||
const validOptions = { bundleId: 'com.valid.app', enableInsecureAuth: false };
|
||||
expect(() => adapter.validateOptions(validOptions)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test payload failing due to missing params or wrong types', function () {
|
||||
it('should throw error for missing authData fields', async function () {
|
||||
await expectAsync(adapter.validateAuthData({})).toBeRejectedWithError(
|
||||
'AuthData id is missing.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test payload fails due to incorrect appId / certificate', function () {
|
||||
it('should throw error for invalid publicKeyUrl', async function () {
|
||||
const invalidPublicKeyUrl = 'https://malicious.url.com/key.cer';
|
||||
|
||||
spyOn(adapter, 'fetchCertificate').and.throwError(
|
||||
new Error('Invalid publicKeyUrl')
|
||||
);
|
||||
|
||||
await expectAsync(
|
||||
adapter.getAppleCertificate(invalidPublicKeyUrl)
|
||||
).toBeRejectedWithError('Invalid publicKeyUrl: https://malicious.url.com/key.cer');
|
||||
});
|
||||
|
||||
it('should throw error for invalid signature verification', async function () {
|
||||
const fakePublicKey = 'invalid-key';
|
||||
const fakeAuthData = {
|
||||
id: '1234567',
|
||||
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||
timestamp: 1460981421303,
|
||||
salt: 'saltST==',
|
||||
signature: 'invalidSignature',
|
||||
};
|
||||
|
||||
spyOn(adapter, 'getAppleCertificate').and.returnValue(Promise.resolve(fakePublicKey));
|
||||
spyOn(adapter, 'verifySignature').and.throwError('Invalid signature.');
|
||||
|
||||
await expectAsync(adapter.validateAuthData(fakeAuthData)).toBeRejectedWithError(
|
||||
'Invalid signature.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test payload passing', function () {
|
||||
it('should successfully process valid payload and save auth data', async function () {
|
||||
const validAuthData = {
|
||||
id: '1234567',
|
||||
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||
timestamp: 1460981421303,
|
||||
salt: 'saltST==',
|
||||
signature: 'validSignature',
|
||||
bundleId: 'com.valid.app',
|
||||
};
|
||||
|
||||
spyOn(adapter, 'getAppleCertificate').and.returnValue(Promise.resolve('validKey'));
|
||||
spyOn(adapter, 'verifySignature').and.returnValue(true);
|
||||
|
||||
await expectAsync(adapter.validateAuthData(validAuthData)).toBeResolved();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Certificate and Signature Validation', function () {
|
||||
it('should fetch and validate Apple certificate', async function () {
|
||||
const certUrl = 'https://static.gc.apple.com/public-key/gc-prod-4.cer';
|
||||
const mockCertificate = 'mockCertificate';
|
||||
|
||||
spyOn(adapter, 'fetchCertificate').and.returnValue(
|
||||
Promise.resolve({ certificate: mockCertificate, headers: new Map() })
|
||||
);
|
||||
spyOn(pki, 'certificateFromPem').and.returnValue({});
|
||||
|
||||
adapter.cache[certUrl] = mockCertificate;
|
||||
|
||||
const cert = await adapter.getAppleCertificate(certUrl);
|
||||
expect(cert).toBe(mockCertificate);
|
||||
});
|
||||
|
||||
it('should verify signature successfully', async function () {
|
||||
const authData = {
|
||||
id: 'G:1965586982',
|
||||
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||
timestamp: 1565257031287,
|
||||
signature:
|
||||
'uqLBTr9Uex8zCpc1UQ1MIDMitb+HUat2Mah4Kw6AVLSGe0gGNJXlih2i5X+0ZwVY0S9zY2NHWi2gFjmhjt/4kxWGMkupqXX5H/qhE2m7hzox6lZJpH98ZEUbouWRfZX2ZhUlCkAX09oRNi7fI7mWL1/o88MaI/y6k6tLr14JTzmlxgdyhw+QRLxRPA6NuvUlRSJpyJ4aGtNH5/wHdKQWL8nUnFYiYmaY8R7IjzNxPfy8UJTUWmeZvMSgND4u8EjADPsz7ZtZyWAPi8kYcAb6M8k0jwLD3vrYCB8XXyO2RQb/FY2TM4zJuI7PzLlvvgOJXbbfVtHx7Evnm5NYoyzgzw==',
|
||||
salt: 'DzqqrQ==',
|
||||
};
|
||||
|
||||
adapter.bundleId = 'cloud.xtralife.gamecenterauth';
|
||||
adapter.enableInsecureAuth = false;
|
||||
|
||||
spyOn(adapter, 'verifyPublicKeyIssuer').and.returnValue();
|
||||
|
||||
const publicKey = await adapter.getAppleCertificate(authData.publicKeyUrl);
|
||||
|
||||
expect(() => adapter.verifySignature(publicKey, authData)).not.toThrow();
|
||||
|
||||
});
|
||||
|
||||
it('should not use bundle id from authData payload in secure mode', async function () {
|
||||
const authData = {
|
||||
id: 'G:1965586982',
|
||||
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||
timestamp: 1565257031287,
|
||||
signature:
|
||||
'uqLBTr9Uex8zCpc1UQ1MIDMitb+HUat2Mah4Kw6AVLSGe0gGNJXlih2i5X+0ZwVY0S9zY2NHWi2gFjmhjt/4kxWGMkupqXX5H/qhE2m7hzox6lZJpH98ZEUbouWRfZX2ZhUlCkAX09oRNi7fI7mWL1/o88MaI/y6k6tLr14JTzmlxgdyhw+QRLxRPA6NuvUlRSJpyJ4aGtNH5/wHdKQWL8nUnFYiYmaY8R7IjzNxPfy8UJTUWmeZvMSgND4u8EjADPsz7ZtZyWAPi8kYcAb6M8k0jwLD3vrYCB8XXyO2RQb/FY2TM4zJuI7PzLlvvgOJXbbfVtHx7Evnm5NYoyzgzw==',
|
||||
salt: 'DzqqrQ==',
|
||||
bundleId: 'com.example.insecure.app',
|
||||
};
|
||||
|
||||
adapter.bundleId = 'cloud.xtralife.gamecenterauth';
|
||||
adapter.enableInsecureAuth = false;
|
||||
|
||||
spyOn(adapter, 'verifyPublicKeyIssuer').and.returnValue();
|
||||
|
||||
const publicKey = await adapter.getAppleCertificate(authData.publicKeyUrl);
|
||||
|
||||
expect(() => adapter.verifySignature(publicKey, authData)).not.toThrow();
|
||||
|
||||
});
|
||||
|
||||
it('should not use bundle id from authData payload in insecure mode', async function () {
|
||||
const authData = {
|
||||
id: 'G:1965586982',
|
||||
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||
timestamp: 1565257031287,
|
||||
signature:
|
||||
'uqLBTr9Uex8zCpc1UQ1MIDMitb+HUat2Mah4Kw6AVLSGe0gGNJXlih2i5X+0ZwVY0S9zY2NHWi2gFjmhjt/4kxWGMkupqXX5H/qhE2m7hzox6lZJpH98ZEUbouWRfZX2ZhUlCkAX09oRNi7fI7mWL1/o88MaI/y6k6tLr14JTzmlxgdyhw+QRLxRPA6NuvUlRSJpyJ4aGtNH5/wHdKQWL8nUnFYiYmaY8R7IjzNxPfy8UJTUWmeZvMSgND4u8EjADPsz7ZtZyWAPi8kYcAb6M8k0jwLD3vrYCB8XXyO2RQb/FY2TM4zJuI7PzLlvvgOJXbbfVtHx7Evnm5NYoyzgzw==',
|
||||
salt: 'DzqqrQ==',
|
||||
bundleId: 'com.example.insecure.app',
|
||||
};
|
||||
|
||||
adapter.bundleId = 'cloud.xtralife.gamecenterauth';
|
||||
adapter.enableInsecureAuth = true;
|
||||
|
||||
spyOn(adapter, 'verifyPublicKeyIssuer').and.returnValue();
|
||||
|
||||
const publicKey = await adapter.getAppleCertificate(authData.publicKeyUrl);
|
||||
|
||||
expect(() => adapter.verifySignature(publicKey, authData)).not.toThrow();
|
||||
|
||||
});
|
||||
|
||||
it('can use bundle id from authData payload in insecure mode', async function () {
|
||||
const authData = {
|
||||
id: 'G:1965586982',
|
||||
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||
timestamp: 1565257031287,
|
||||
signature:
|
||||
'uqLBTr9Uex8zCpc1UQ1MIDMitb+HUat2Mah4Kw6AVLSGe0gGNJXlih2i5X+0ZwVY0S9zY2NHWi2gFjmhjt/4kxWGMkupqXX5H/qhE2m7hzox6lZJpH98ZEUbouWRfZX2ZhUlCkAX09oRNi7fI7mWL1/o88MaI/y6k6tLr14JTzmlxgdyhw+QRLxRPA6NuvUlRSJpyJ4aGtNH5/wHdKQWL8nUnFYiYmaY8R7IjzNxPfy8UJTUWmeZvMSgND4u8EjADPsz7ZtZyWAPi8kYcAb6M8k0jwLD3vrYCB8XXyO2RQb/FY2TM4zJuI7PzLlvvgOJXbbfVtHx7Evnm5NYoyzgzw==',
|
||||
salt: 'DzqqrQ==',
|
||||
bundleId: 'cloud.xtralife.gamecenterauth',
|
||||
};
|
||||
|
||||
adapter.enableInsecureAuth = true;
|
||||
|
||||
spyOn(adapter, 'verifyPublicKeyIssuer').and.returnValue();
|
||||
|
||||
const publicKey = await adapter.getAppleCertificate(authData.publicKeyUrl);
|
||||
|
||||
expect(() => adapter.verifySignature(publicKey, authData)).not.toThrow();
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,285 +0,0 @@
|
||||
const GitHubAdapter = require('../../../lib/Adapters/Auth/github').default;
|
||||
|
||||
describe('GitHubAdapter', function () {
|
||||
let adapter;
|
||||
const validOptions = {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new GitHubAdapter.constructor();
|
||||
adapter.validateOptions(validOptions);
|
||||
});
|
||||
|
||||
describe('getAccessTokenFromCode', function () {
|
||||
it('should fetch an access token successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://github.com/login/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const code = 'validCode';
|
||||
const token = await adapter.getAccessTokenFromCode(code);
|
||||
|
||||
expect(token).toBe('mockAccessToken');
|
||||
});
|
||||
|
||||
it('should throw an error if the response is not ok', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://github.com/login/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Bad Request',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const code = 'invalidCode';
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(code)).toBeRejectedWithError(
|
||||
'Failed to exchange code for token: Bad Request'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if the response contains an error', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://github.com/login/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
error: 'invalid_grant',
|
||||
error_description: 'Code is invalid',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const code = 'invalidCode';
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(code)).toBeRejectedWithError('Code is invalid');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserFromAccessToken', function () {
|
||||
it('should fetch user data successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.github.com/user',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'mockUserId',
|
||||
login: 'mockUserLogin',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'validAccessToken';
|
||||
const user = await adapter.getUserFromAccessToken(accessToken);
|
||||
|
||||
expect(user).toEqual({ id: 'mockUserId', login: 'mockUserLogin' });
|
||||
});
|
||||
|
||||
it('should throw an error if the response is not ok', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.github.com/user',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'invalidAccessToken';
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken(accessToken)).toBeRejectedWithError(
|
||||
'Failed to fetch GitHub user: Unauthorized'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if user data is invalid', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.github.com/user',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'validAccessToken';
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken(accessToken)).toBeRejectedWithError(
|
||||
'Invalid GitHub user data received.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GitHubAdapter E2E Test', function () {
|
||||
beforeEach(async function () {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
github: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should log in user using GitHub adapter successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://github.com/login/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.github.com/user',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'mockUserId',
|
||||
login: 'mockUserLogin',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode' };
|
||||
const user = await Parse.User.logInWith('github', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle error when GitHub returns invalid code', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://github.com/login/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Invalid code',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'invalidCode' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('github', { authData })).toBeRejectedWithError(
|
||||
'Failed to exchange code for token: Invalid code'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when GitHub returns invalid user data', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://github.com/login/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.github.com/user',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('github', { authData })).toBeRejectedWithError(
|
||||
'Failed to fetch GitHub user: Unauthorized'
|
||||
);
|
||||
});
|
||||
|
||||
it('e2e secure does not support insecure payload', async function () {
|
||||
mockFetch();
|
||||
const authData = { id: 'mockUserId', access_token: 'mockAccessToken123' };
|
||||
await expectAsync(Parse.User.logInWith('github', { authData })).toBeRejectedWithError(
|
||||
'GitHub code is required.'
|
||||
);
|
||||
});
|
||||
|
||||
it('e2e insecure does support secure payload', async function () {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
github: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
enableInsecureAuth: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://github.com/login/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.github.com/user',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'mockUserId',
|
||||
login: 'mockUserLogin',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode' };
|
||||
const user = await Parse.User.logInWith('github', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,356 +0,0 @@
|
||||
const GooglePlayGamesServicesAdapter = require('../../../lib/Adapters/Auth/gpgames').default;
|
||||
|
||||
describe('GooglePlayGamesServicesAdapter', function () {
|
||||
let adapter;
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new GooglePlayGamesServicesAdapter.constructor();
|
||||
adapter.clientId = 'validClientId';
|
||||
adapter.clientSecret = 'validClientSecret';
|
||||
});
|
||||
|
||||
describe('getAccessTokenFromCode', function () {
|
||||
it('should fetch an access token successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://oauth2.googleapis.com/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const code = 'validCode';
|
||||
const authData = { redirectUri: 'http://example.com' };
|
||||
const token = await adapter.getAccessTokenFromCode(code, authData);
|
||||
|
||||
expect(token).toBe('mockAccessToken');
|
||||
});
|
||||
|
||||
it('should throw an error if the response is not ok', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://oauth2.googleapis.com/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Bad Request',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const code = 'invalidCode';
|
||||
const authData = { redirectUri: 'http://example.com' };
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(code, authData)).toBeRejectedWithError(
|
||||
'Failed to exchange code for token: Bad Request'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if the response contains an error', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://oauth2.googleapis.com/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
error: 'invalid_grant',
|
||||
error_description: 'Code is invalid',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const code = 'invalidCode';
|
||||
const authData = { redirectUri: 'http://example.com' };
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(code, authData)).toBeRejectedWithError(
|
||||
'Code is invalid'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserFromAccessToken', function () {
|
||||
it('should fetch user data successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
playerId: 'mockUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'validAccessToken';
|
||||
const authData = { id: 'mockUserId' };
|
||||
const user = await adapter.getUserFromAccessToken(accessToken, authData);
|
||||
|
||||
expect(user).toEqual({ id: 'mockUserId' });
|
||||
});
|
||||
|
||||
it('should throw an error if the response is not ok', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'invalidAccessToken';
|
||||
const authData = { id: 'mockUserId' };
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken(accessToken, authData)).toBeRejectedWithError(
|
||||
'Failed to fetch Google Play Games Services user: Unauthorized'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if user data is invalid', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'validAccessToken';
|
||||
const authData = { id: 'mockUserId' };
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken(accessToken, authData)).toBeRejectedWithError(
|
||||
'Invalid Google Play Games Services user data received.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if playerId does not match the provided user ID', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
playerId: 'anotherUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'validAccessToken';
|
||||
const authData = { id: 'mockUserId' };
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken(accessToken, authData)).toBeRejectedWithError(
|
||||
'Invalid Google Play Games Services user data received.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GooglePlayGamesServicesAdapter E2E Test', function () {
|
||||
beforeEach(async function () {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
gpgames: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should log in user successfully with valid code', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://oauth2.googleapis.com/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
playerId: 'mockUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
id: 'mockUserId',
|
||||
redirectUri: 'http://example.com',
|
||||
};
|
||||
|
||||
const user = await Parse.User.logInWith('gpgames', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://oauth2.googleapis.com/token',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when the token exchange fails', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://oauth2.googleapis.com/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Invalid code',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'invalidCode',
|
||||
redirectUri: 'http://example.com',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('gpgames', { authData })).toBeRejectedWithError(
|
||||
'Failed to exchange code for token: Invalid code'
|
||||
);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://oauth2.googleapis.com/token',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when user data fetch fails', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://oauth2.googleapis.com/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
id: 'mockUserId',
|
||||
redirectUri: 'http://example.com',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('gpgames', { authData })).toBeRejectedWithError(
|
||||
'Failed to fetch Google Play Games Services user: Unauthorized'
|
||||
);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://oauth2.googleapis.com/token',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when user data is invalid', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://oauth2.googleapis.com/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
playerId: 'anotherUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
id: 'mockUserId',
|
||||
redirectUri: 'http://example.com',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('gpgames', { authData })).toBeRejectedWithError(
|
||||
'Invalid Google Play Games Services user data received.'
|
||||
);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://oauth2.googleapis.com/token',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when no code or access token is provided', async function () {
|
||||
mockFetch();
|
||||
|
||||
const authData = {
|
||||
id: 'mockUserId',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('gpgames', { authData })).toBeRejectedWithError(
|
||||
'gpgames code is required.'
|
||||
);
|
||||
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -1,258 +0,0 @@
|
||||
const InstagramAdapter = require('../../../lib/Adapters/Auth/instagram').default;
|
||||
|
||||
describe('InstagramAdapter', function () {
|
||||
let adapter;
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new InstagramAdapter.constructor();
|
||||
adapter.clientId = 'validClientId';
|
||||
adapter.clientSecret = 'validClientSecret';
|
||||
adapter.redirectUri = 'https://example.com/callback';
|
||||
});
|
||||
|
||||
describe('getAccessTokenFromCode', function () {
|
||||
it('should fetch an access token successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.instagram.com/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode' };
|
||||
const token = await adapter.getAccessTokenFromCode(authData);
|
||||
|
||||
expect(token).toBe('mockAccessToken');
|
||||
});
|
||||
|
||||
it('should throw an error if the response contains an error', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.instagram.com/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
error: 'invalid_grant',
|
||||
error_description: 'Code is invalid',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'invalidCode' };
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
|
||||
'Code is invalid'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserFromAccessToken', function () {
|
||||
it('should fetch user data successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'mockUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'mockAccessToken';
|
||||
const authData = { id: 'mockUserId' };
|
||||
const user = await adapter.getUserFromAccessToken(accessToken, authData);
|
||||
|
||||
expect(user).toEqual({ id: 'mockUserId' });
|
||||
});
|
||||
|
||||
it('should throw an error if user ID does not match authData', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'differentUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'mockAccessToken';
|
||||
const authData = { id: 'mockUserId' };
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken(accessToken, authData)).toBeRejectedWithError(
|
||||
'Instagram auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('InstagramAdapter E2E Test', function () {
|
||||
beforeEach(async function () {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
instagram: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
redirectUri: 'https://example.com/callback',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should log in user successfully with valid code', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.instagram.com/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken123',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'mockUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
id: 'mockUserId',
|
||||
};
|
||||
|
||||
const user = await Parse.User.logInWith('instagram', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle error when access token exchange fails', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.instagram.com/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Invalid code',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'invalidCode',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('instagram', { authData })).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Instagram API request failed.')
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when user data fetch fails', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.instagram.com/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken123',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
id: 'mockUserId',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('instagram', { authData })).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Instagram API request failed.')
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when user data is invalid', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.instagram.com/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken123',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'differentUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
id: 'mockUserId',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('instagram', { authData })).toBeRejectedWithError(
|
||||
'Instagram auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when no code or access token is provided', async function () {
|
||||
mockFetch();
|
||||
|
||||
const authData = {
|
||||
id: 'mockUserId',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('instagram', { authData })).toBeRejectedWithError(
|
||||
'Instagram code is required.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,309 +0,0 @@
|
||||
const LineAdapter = require('../../../lib/Adapters/Auth/line').default;
|
||||
describe('LineAdapter', function () {
|
||||
let adapter;
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new LineAdapter.constructor();
|
||||
adapter.clientId = 'validClientId';
|
||||
adapter.clientSecret = 'validClientSecret';
|
||||
});
|
||||
|
||||
describe('getAccessTokenFromCode', function () {
|
||||
it('should throw an error if code is missing in authData', async function () {
|
||||
const authData = { redirect_uri: 'http://example.com' };
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
|
||||
'Line auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should fetch an access token successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/oauth2/v2.1/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
redirect_uri: 'http://example.com',
|
||||
};
|
||||
|
||||
const token = await adapter.getAccessTokenFromCode(authData);
|
||||
|
||||
expect(token).toBe('mockAccessToken');
|
||||
});
|
||||
|
||||
it('should throw an error if response is not ok', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/oauth2/v2.1/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Bad Request',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'invalidCode',
|
||||
redirect_uri: 'http://example.com',
|
||||
};
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
|
||||
'Failed to exchange code for token: Bad Request'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if response contains an error object', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/oauth2/v2.1/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
error: 'invalid_grant',
|
||||
error_description: 'Code is invalid',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'invalidCode',
|
||||
redirect_uri: 'http://example.com',
|
||||
};
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
|
||||
'Code is invalid'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserFromAccessToken', function () {
|
||||
it('should fetch user data successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/v2/profile',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
userId: 'mockUserId',
|
||||
displayName: 'mockDisplayName',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'validAccessToken';
|
||||
const user = await adapter.getUserFromAccessToken(accessToken);
|
||||
|
||||
expect(user).toEqual({
|
||||
userId: 'mockUserId',
|
||||
displayName: 'mockDisplayName',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if response is not ok', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/v2/profile',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'invalidAccessToken';
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken(accessToken)).toBeRejectedWithError(
|
||||
'Failed to fetch Line user: Unauthorized'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if user data is invalid', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/v2/profile',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'validAccessToken';
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken(accessToken)).toBeRejectedWithError(
|
||||
'Invalid Line user data received.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('LineAdapter E2E Test', function () {
|
||||
beforeEach(async function () {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
line: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should log in user successfully with valid code', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/oauth2/v2.1/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.line.me/v2/profile',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
userId: 'mockUserId',
|
||||
displayName: 'mockDisplayName',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
redirect_uri: 'http://example.com',
|
||||
};
|
||||
|
||||
const user = await Parse.User.logInWith('line', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle error when token exchange fails', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/oauth2/v2.1/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Invalid code',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'invalidCode',
|
||||
redirect_uri: 'http://example.com',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('line', { authData })).toBeRejectedWithError(
|
||||
'Failed to exchange code for token: Invalid code'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when user data fetch fails', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/oauth2/v2.1/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.line.me/v2/profile',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
redirect_uri: 'http://example.com',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('line', { authData })).toBeRejectedWithError(
|
||||
'Failed to fetch Line user: Unauthorized'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when user data is invalid', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/oauth2/v2.1/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.line.me/v2/profile',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
redirect_uri: 'http://example.com',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('line', { authData })).toBeRejectedWithError(
|
||||
'Invalid Line user data received.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when no code is provided', async function () {
|
||||
mockFetch();
|
||||
|
||||
const authData = {
|
||||
redirect_uri: 'http://example.com',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('line', { authData })).toBeRejectedWithError(
|
||||
'Line code is required.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,333 +0,0 @@
|
||||
|
||||
const LinkedInAdapter = require('../../../lib/Adapters/Auth/linkedin').default;
|
||||
describe('LinkedInAdapter', function () {
|
||||
let adapter;
|
||||
const validOptions = {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
enableInsecureAuth: false,
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new LinkedInAdapter.constructor();
|
||||
});
|
||||
|
||||
describe('Test configuration errors', function () {
|
||||
it('should throw error for missing options', function () {
|
||||
const invalidOptions = [null, undefined, {}, { clientId: 'validClientId' }];
|
||||
|
||||
for (const options of invalidOptions) {
|
||||
expect(() => {
|
||||
adapter.validateOptions(options);
|
||||
}).toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
it('should validate options successfully with valid parameters', function () {
|
||||
expect(() => {
|
||||
adapter.validateOptions(validOptions);
|
||||
}).not.toThrow();
|
||||
expect(adapter.clientId).toBe(validOptions.clientId);
|
||||
expect(adapter.clientSecret).toBe(validOptions.clientSecret);
|
||||
expect(adapter.enableInsecureAuth).toBe(validOptions.enableInsecureAuth);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test beforeFind', function () {
|
||||
it('should throw error for invalid payload', async function () {
|
||||
adapter.enableInsecureAuth = true;
|
||||
|
||||
const payloads = [{}, { access_token: null }];
|
||||
|
||||
for (const payload of payloads) {
|
||||
await expectAsync(adapter.beforeFind(payload)).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'LinkedIn auth is invalid for this user.')
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should process secure payload and set auth data', async function () {
|
||||
spyOn(adapter, 'getAccessTokenFromCode').and.returnValue(
|
||||
Promise.resolve('validToken')
|
||||
);
|
||||
spyOn(adapter, 'getUserFromAccessToken').and.returnValue(
|
||||
Promise.resolve({ id: 'validUserId' })
|
||||
);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com', is_mobile_sdk: false };
|
||||
|
||||
await adapter.beforeFind(authData);
|
||||
|
||||
expect(authData.access_token).toBe('validToken');
|
||||
expect(authData.id).toBe('validUserId');
|
||||
});
|
||||
|
||||
it('should validate insecure auth and match user id', async function () {
|
||||
adapter.enableInsecureAuth = true;
|
||||
spyOn(adapter, 'getUserFromAccessToken').and.returnValue(
|
||||
Promise.resolve({ id: 'validUserId' })
|
||||
);
|
||||
|
||||
const authData = { access_token: 'validToken', id: 'validUserId', is_mobile_sdk: false };
|
||||
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeResolved();
|
||||
});
|
||||
|
||||
it('should throw error if insecure auth user id does not match', async function () {
|
||||
adapter.enableInsecureAuth = true;
|
||||
spyOn(adapter, 'getUserFromAccessToken').and.returnValue(
|
||||
Promise.resolve({ id: 'invalidUserId' })
|
||||
);
|
||||
|
||||
const authData = { access_token: 'validToken', id: 'validUserId', is_mobile_sdk: false };
|
||||
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeRejectedWith(
|
||||
new Error('LinkedIn auth is invalid for this user.')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test getUserFromAccessToken', function () {
|
||||
it('should fetch user successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.linkedin.com/v2/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ id: 'validUserId' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const user = await adapter.getUserFromAccessToken('validToken', false);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith('https://api.linkedin.com/v2/me', {
|
||||
headers: {
|
||||
Authorization: `Bearer validToken`,
|
||||
'x-li-format': 'json',
|
||||
'x-li-src': undefined,
|
||||
},
|
||||
method: 'GET',
|
||||
});
|
||||
expect(user).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
|
||||
it('should throw error for invalid response', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.linkedin.com/v2/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken('invalidToken', false)).toBeRejectedWith(
|
||||
new Error('LinkedIn API request failed.')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test getAccessTokenFromCode', function () {
|
||||
it('should fetch token successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://www.linkedin.com/oauth/v2/accessToken',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validToken' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const tokenResponse = await adapter.getAccessTokenFromCode('validCode', 'http://example.com');
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith('https://www.linkedin.com/oauth/v2/accessToken', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: jasmine.any(URLSearchParams),
|
||||
});
|
||||
expect(tokenResponse).toEqual('validToken');
|
||||
});
|
||||
|
||||
it('should throw error for invalid response', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://www.linkedin.com/oauth/v2/accessToken',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(
|
||||
adapter.getAccessTokenFromCode('invalidCode', 'http://example.com')
|
||||
).toBeRejectedWith(new Error('LinkedIn API request failed.'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test validate methods', function () {
|
||||
const authData = { id: 'validUserId', access_token: 'validToken' };
|
||||
|
||||
it('validateLogin should return user id', function () {
|
||||
const result = adapter.validateLogin(authData);
|
||||
expect(result).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
|
||||
it('validateSetUp should return user id', function () {
|
||||
const result = adapter.validateSetUp(authData);
|
||||
expect(result).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
|
||||
it('validateUpdate should return user id', function () {
|
||||
const result = adapter.validateUpdate(authData);
|
||||
expect(result).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
|
||||
it('afterFind should return user id', function () {
|
||||
const result = adapter.afterFind(authData);
|
||||
expect(result).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('LinkedInAdapter E2E Test', function () {
|
||||
beforeEach(async function () {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
linkedin: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should log in user using LinkedIn adapter successfully (secure)', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://www.linkedin.com/oauth/v2/accessToken',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.linkedin.com/v2/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'mockUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'https://example.com/callback' };
|
||||
const user = await Parse.User.logInWith('linkedin', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://www.linkedin.com/oauth/v2/accessToken',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://api.linkedin.com/v2/me',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when LinkedIn returns invalid user data', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://www.linkedin.com/oauth/v2/accessToken',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.linkedin.com/v2/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'https://example.com/callback' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('linkedin', { authData })).toBeRejectedWithError(
|
||||
'LinkedIn API request failed.'
|
||||
);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://www.linkedin.com/oauth/v2/accessToken',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://api.linkedin.com/v2/me',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('secure does not support insecure payload if not enabled', async function () {
|
||||
mockFetch();
|
||||
const authData = { id: 'mockUserId', access_token: 'mockAccessToken123' };
|
||||
await expectAsync(Parse.User.logInWith('linkedin', { authData })).toBeRejectedWithError(
|
||||
'LinkedIn code is required.'
|
||||
);
|
||||
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('insecure mode supports insecure payload if enabled', async function () {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
linkedin: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
enableInsecureAuth: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.linkedin.com/v2/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'mockUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { id: 'mockUserId', access_token: 'mockAccessToken123' };
|
||||
const user = await Parse.User.logInWith('linkedin', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://api.linkedin.com/v2/me',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,307 +0,0 @@
|
||||
const MicrosoftAdapter = require('../../../lib/Adapters/Auth/microsoft').default;
|
||||
|
||||
describe('MicrosoftAdapter', function () {
|
||||
let adapter;
|
||||
const validOptions = {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
enableInsecureAuth: false,
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new MicrosoftAdapter.constructor();
|
||||
});
|
||||
|
||||
describe('Test configuration errors', function () {
|
||||
it('should throw error for missing options', function () {
|
||||
const invalidOptions = [null, undefined, {}, { clientId: 'validClientId' }];
|
||||
|
||||
for (const options of invalidOptions) {
|
||||
expect(() => {
|
||||
adapter.validateOptions(options);
|
||||
}).toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
it('should validate options successfully with valid parameters', function () {
|
||||
expect(() => {
|
||||
adapter.validateOptions(validOptions);
|
||||
}).not.toThrow();
|
||||
expect(adapter.clientId).toBe(validOptions.clientId);
|
||||
expect(adapter.clientSecret).toBe(validOptions.clientSecret);
|
||||
expect(adapter.enableInsecureAuth).toBe(validOptions.enableInsecureAuth);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test getUserFromAccessToken', function () {
|
||||
it('should fetch user successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.microsoft.com/v1.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ id: 'validUserId' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const user = await adapter.getUserFromAccessToken('validToken');
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith('https://graph.microsoft.com/v1.0/me', {
|
||||
headers: {
|
||||
Authorization: 'Bearer validToken',
|
||||
},
|
||||
method: 'GET',
|
||||
});
|
||||
expect(user).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
|
||||
it('should throw error for invalid response', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.microsoft.com/v1.0/me',
|
||||
method: 'GET',
|
||||
response: { ok: false },
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken('invalidToken')).toBeRejectedWith(
|
||||
new Error('Microsoft API request failed.')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test getAccessTokenFromCode', function () {
|
||||
it('should fetch token successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validToken' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com' };
|
||||
const token = await adapter.getAccessTokenFromCode(authData);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: jasmine.any(URLSearchParams),
|
||||
});
|
||||
expect(token).toEqual('validToken');
|
||||
});
|
||||
|
||||
it('should throw error for invalid response', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
method: 'POST',
|
||||
response: { ok: false },
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'invalidCode', redirect_uri: 'http://example.com' };
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWith(
|
||||
new Error('Microsoft API request failed.')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test secure authentication flow', function () {
|
||||
it('should exchange code for access token and fetch user data', async function () {
|
||||
spyOn(adapter, 'getAccessTokenFromCode').and.returnValue(Promise.resolve('validToken'));
|
||||
spyOn(adapter, 'getUserFromAccessToken').and.returnValue(Promise.resolve({ id: 'validUserId' }));
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com' };
|
||||
await adapter.beforeFind(authData);
|
||||
|
||||
expect(authData.access_token).toBe('validToken');
|
||||
expect(authData.id).toBe('validUserId');
|
||||
});
|
||||
|
||||
it('should throw error if user data cannot be fetched', async function () {
|
||||
spyOn(adapter, 'getAccessTokenFromCode').and.returnValue(Promise.resolve('validToken'));
|
||||
spyOn(adapter, 'getUserFromAccessToken').and.throwError('Microsoft API request failed.');
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com' };
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeRejectedWith(
|
||||
new Error('Microsoft API request failed.')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test insecure authentication flow', function () {
|
||||
beforeEach(function () {
|
||||
adapter.enableInsecureAuth = true;
|
||||
});
|
||||
|
||||
it('should validate insecure auth and match user id', async function () {
|
||||
spyOn(adapter, 'getUserFromAccessToken').and.returnValue(
|
||||
Promise.resolve({ id: 'validUserId' })
|
||||
);
|
||||
|
||||
const authData = { access_token: 'validToken', id: 'validUserId' };
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeResolved();
|
||||
});
|
||||
|
||||
it('should throw error if insecure auth user id does not match', async function () {
|
||||
spyOn(adapter, 'getUserFromAccessToken').and.returnValue(
|
||||
Promise.resolve({ id: 'invalidUserId' })
|
||||
);
|
||||
|
||||
const authData = { access_token: 'validToken', id: 'validUserId' };
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeRejectedWith(
|
||||
new Error('Microsoft auth is invalid for this user.')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MicrosoftAdapter E2E Tests', () => {
|
||||
beforeEach(async () => {
|
||||
// Simulate reconfiguring the server with Microsoft auth options
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
microsoft: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
enableInsecureAuth: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should authenticate user successfully using MicrosoftAdapter', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validAccessToken' }),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://graph.microsoft.com/v1.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ id: 'user123' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
|
||||
const user = await Parse.User.logInWith('microsoft', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle invalid code error gracefully', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
method: 'POST',
|
||||
response: { ok: false, statusText: 'Invalid code' },
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'invalidCode', redirect_uri: 'http://example.com/callback' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('microsoft', { authData })).toBeRejectedWithError(
|
||||
'Microsoft API request failed.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when fetching user data fails', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validAccessToken' }),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://graph.microsoft.com/v1.0/me',
|
||||
method: 'GET',
|
||||
response: { ok: false, statusText: 'Unauthorized' },
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('microsoft', { authData })).toBeRejectedWithError(
|
||||
'Microsoft API request failed.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow insecure auth when enabled', async () => {
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.microsoft.com/v1.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
id: 'user123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
microsoft: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
enableInsecureAuth: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const authData = { access_token: 'validAccessToken', id: 'user123' };
|
||||
const user = await Parse.User.logInWith('microsoft', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should reject insecure auth when user id does not match', async () => {
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.microsoft.com/v1.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
id: 'incorrectUser',
|
||||
}),
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
microsoft: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
enableInsecureAuth: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const authData = { access_token: 'validAccessToken', id: 'incorrectUserId' };
|
||||
await expectAsync(Parse.User.logInWith('microsoft', { authData })).toBeRejectedWithError(
|
||||
'Microsoft auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,305 +0,0 @@
|
||||
const OAuth2Adapter = require('../../../lib/Adapters/Auth/oauth2').default;
|
||||
|
||||
describe('OAuth2Adapter', () => {
|
||||
let adapter;
|
||||
|
||||
const validOptions = {
|
||||
tokenIntrospectionEndpointUrl: 'https://provider.com/introspect',
|
||||
useridField: 'sub',
|
||||
appidField: 'aud',
|
||||
appIds: ['valid-app-id'],
|
||||
authorizationHeader: 'Bearer validAuthToken',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
adapter = new OAuth2Adapter.constructor();
|
||||
adapter.validateOptions(validOptions);
|
||||
});
|
||||
|
||||
describe('validateAppId', () => {
|
||||
it('should validate app ID successfully', async () => {
|
||||
const authData = { access_token: 'validAccessToken' };
|
||||
const mockResponse = {
|
||||
[validOptions.appidField]: 'valid-app-id',
|
||||
};
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: validOptions.tokenIntrospectionEndpointUrl,
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(
|
||||
adapter.validateAppId(validOptions.appIds, authData, validOptions)
|
||||
).toBeResolved();
|
||||
});
|
||||
|
||||
it('should throw an error if app ID is invalid', async () => {
|
||||
const authData = { access_token: 'validAccessToken' };
|
||||
const mockResponse = {
|
||||
[validOptions.appidField]: 'invalid-app-id',
|
||||
};
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: validOptions.tokenIntrospectionEndpointUrl,
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(
|
||||
adapter.validateAppId(validOptions.appIds, authData, validOptions)
|
||||
).toBeRejectedWithError('OAuth2: Invalid app ID.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateAuthData', () => {
|
||||
it('should validate auth data successfully', async () => {
|
||||
const authData = { id: 'user-id', access_token: 'validAccessToken' };
|
||||
const mockResponse = {
|
||||
active: true,
|
||||
[validOptions.useridField]: 'user-id',
|
||||
};
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: validOptions.tokenIntrospectionEndpointUrl,
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(
|
||||
adapter.validateAuthData(authData, null, validOptions)
|
||||
).toBeResolvedTo({});
|
||||
});
|
||||
|
||||
it('should throw an error if the token is inactive', async () => {
|
||||
const authData = { id: 'user-id', access_token: 'validAccessToken' };
|
||||
const mockResponse = { active: false };
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: validOptions.tokenIntrospectionEndpointUrl,
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(
|
||||
adapter.validateAuthData(authData, null, validOptions)
|
||||
).toBeRejectedWith(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 access token is invalid for this user.'));
|
||||
});
|
||||
|
||||
it('should throw an error if user ID does not match', async () => {
|
||||
const authData = { id: 'user-id', access_token: 'validAccessToken' };
|
||||
const mockResponse = {
|
||||
active: true,
|
||||
[validOptions.useridField]: 'different-user-id',
|
||||
};
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: validOptions.tokenIntrospectionEndpointUrl,
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(
|
||||
adapter.validateAuthData(authData, null, validOptions)
|
||||
).toBeRejectedWithError('OAuth2 access token is invalid for this user.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('requestTokenInfo', () => {
|
||||
it('should fetch token info successfully', async () => {
|
||||
const mockResponse = { active: true };
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: validOptions.tokenIntrospectionEndpointUrl,
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await adapter.requestTokenInfo(
|
||||
'validAccessToken',
|
||||
validOptions
|
||||
);
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('should throw an error if the introspection endpoint URL is missing', async () => {
|
||||
const options = { ...validOptions, tokenIntrospectionEndpointUrl: null };
|
||||
|
||||
expect(
|
||||
() => adapter.validateOptions(options)
|
||||
).toThrow(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 token introspection endpoint URL is missing.'));
|
||||
});
|
||||
|
||||
it('should throw an error if the response is not ok', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: validOptions.tokenIntrospectionEndpointUrl,
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Bad Request',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(
|
||||
adapter.requestTokenInfo('invalidAccessToken')
|
||||
).toBeRejectedWithError('OAuth2 token introspection request failed.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('OAuth2Adapter E2E Tests', () => {
|
||||
beforeEach(async () => {
|
||||
// Simulate reconfiguring the server with OAuth2 auth options
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
mockOauth: {
|
||||
tokenIntrospectionEndpointUrl: 'https://provider.com/introspect',
|
||||
useridField: 'sub',
|
||||
appidField: 'aud',
|
||||
appIds: ['valid-app-id'],
|
||||
authorizationHeader: 'Bearer validAuthToken',
|
||||
oauth2: true
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should validate and authenticate user successfully', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://provider.com/introspect',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
active: true,
|
||||
sub: 'user123',
|
||||
aud: 'valid-app-id',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { access_token: 'validAccessToken', id: 'user123' };
|
||||
const user = await Parse.User.logInWith('mockOauth', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
expect(user.get('authData').mockOauth.id).toEqual('user123');
|
||||
});
|
||||
|
||||
it('should reject authentication for inactive token', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://provider.com/introspect',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ active: false, aud: ['valid-app-id'] }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { access_token: 'inactiveToken', id: 'user123' };
|
||||
await expectAsync(Parse.User.logInWith('mockOauth', { authData })).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 access token is invalid for this user.')
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject authentication for mismatched user ID', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://provider.com/introspect',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
active: true,
|
||||
sub: 'different-user',
|
||||
aud: 'valid-app-id',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { access_token: 'validAccessToken', id: 'user123' };
|
||||
await expectAsync(Parse.User.logInWith('mockOauth', { authData })).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 access token is invalid for this user.')
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject authentication for invalid app ID', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://provider.com/introspect',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
active: true,
|
||||
sub: 'user123',
|
||||
aud: 'invalid-app-id',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { access_token: 'validAccessToken', id: 'user123' };
|
||||
await expectAsync(Parse.User.logInWith('mockOauth', { authData })).toBeRejectedWithError(
|
||||
'OAuth2: Invalid app ID.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when token introspection endpoint is missing', async () => {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
mockOauth: {
|
||||
tokenIntrospectionEndpointUrl: null,
|
||||
useridField: 'sub',
|
||||
appidField: 'aud',
|
||||
appIds: ['valid-app-id'],
|
||||
authorizationHeader: 'Bearer validAuthToken',
|
||||
oauth2: true
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const authData = { access_token: 'validAccessToken', id: 'user123' };
|
||||
await expectAsync(Parse.User.logInWith('mockOauth', { authData })).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 token introspection endpoint URL is missing.')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,252 +0,0 @@
|
||||
const QqAdapter = require('../../../lib/Adapters/Auth/qq').default;
|
||||
|
||||
describe('QqAdapter', () => {
|
||||
let adapter;
|
||||
|
||||
beforeEach(() => {
|
||||
adapter = new QqAdapter.constructor();
|
||||
});
|
||||
|
||||
describe('getUserFromAccessToken', () => {
|
||||
it('should fetch user data successfully', async () => {
|
||||
const mockResponse = `callback({"client_id":"validAppId","openid":"user123"})`;
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
text: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await adapter.getUserFromAccessToken('validAccessToken');
|
||||
|
||||
expect(result).toEqual({ client_id: 'validAppId', openid: 'user123' });
|
||||
});
|
||||
|
||||
it('should throw an error if the API request fails', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(
|
||||
adapter.getUserFromAccessToken('invalidAccessToken')
|
||||
).toBeRejectedWithError('qq API request failed.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAccessTokenFromCode', () => {
|
||||
it('should fetch access token successfully', async () => {
|
||||
const mockResponse = `callback({"access_token":"validAccessToken","expires_in":3600,"refresh_token":"refreshToken"})`;
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/token',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
text: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await adapter.getAccessTokenFromCode({
|
||||
code: 'validCode',
|
||||
redirect_uri: 'https://your-redirect-uri.com/callback',
|
||||
});
|
||||
|
||||
expect(result).toBe('validAccessToken');
|
||||
});
|
||||
|
||||
it('should throw an error if the API request fails', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/token',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Bad Request',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(
|
||||
adapter.getAccessTokenFromCode({
|
||||
code: 'invalidCode',
|
||||
redirect_uri: 'https://your-redirect-uri.com/callback',
|
||||
})
|
||||
).toBeRejectedWithError('qq API request failed.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseResponseData', () => {
|
||||
it('should parse valid callback response data', () => {
|
||||
const response = `callback({"key":"value"})`;
|
||||
const result = adapter.parseResponseData(response);
|
||||
|
||||
expect(result).toEqual({ key: 'value' });
|
||||
});
|
||||
|
||||
it('should throw an error if the response data is invalid', () => {
|
||||
const response = 'invalid response';
|
||||
|
||||
expect(() => adapter.parseResponseData(response)).toThrowError(
|
||||
'qq auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('QqAdapter E2E Test', () => {
|
||||
beforeEach(async () => {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
qq: {
|
||||
clientId: 'validAppId',
|
||||
clientSecret: 'validAppSecret',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should log in user using Qq adapter successfully', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/token',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
text: () =>
|
||||
Promise.resolve(
|
||||
`callback({"access_token":"mockAccessToken","expires_in":3600})`
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
text: () =>
|
||||
Promise.resolve(
|
||||
`callback({"client_id":"validAppId","openid":"user123"})`
|
||||
),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'https://your-redirect-uri.com/callback' };
|
||||
const user = await Parse.User.logInWith('qq', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle error when Qq returns invalid code', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/token',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Invalid code',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'invalidCode', redirect_uri: 'https://your-redirect-uri.com/callback' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('qq', { authData })).toBeRejectedWithError(
|
||||
'qq API request failed.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when Qq returns invalid user data', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/token',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
text: () =>
|
||||
Promise.resolve(
|
||||
`callback({"access_token":"mockAccessToken","expires_in":3600})`
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'https://your-redirect-uri.com/callback' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('qq', { authData })).toBeRejectedWithError(
|
||||
'qq API request failed.'
|
||||
);
|
||||
});
|
||||
|
||||
it('e2e secure does not support insecure payload', async () => {
|
||||
mockFetch();
|
||||
const authData = { id: 'mockUserId', access_token: 'mockAccessToken' };
|
||||
await expectAsync(Parse.User.logInWith('qq', { authData })).toBeRejectedWithError(
|
||||
'qq code is required.'
|
||||
);
|
||||
});
|
||||
|
||||
it('e2e insecure does support secure payload', async () => {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
qq: {
|
||||
appId: 'validAppId',
|
||||
appSecret: 'validAppSecret',
|
||||
enableInsecureAuth: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/token',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
text: () =>
|
||||
Promise.resolve(
|
||||
`callback({"access_token":"mockAccessToken","expires_in":3600})`
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
text: () =>
|
||||
Promise.resolve(
|
||||
`callback({"client_id":"validAppId","openid":"user123"})`
|
||||
),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'https://your-redirect-uri.com/callback' };
|
||||
const user = await Parse.User.logInWith('qq', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,113 +0,0 @@
|
||||
const SpotifyAdapter = require('../../../lib/Adapters/Auth/spotify').default;
|
||||
|
||||
describe('SpotifyAdapter', () => {
|
||||
let adapter;
|
||||
|
||||
beforeEach(() => {
|
||||
adapter = new SpotifyAdapter.constructor();
|
||||
});
|
||||
|
||||
describe('getUserFromAccessToken', () => {
|
||||
it('should fetch user data successfully', async () => {
|
||||
const mockResponse = {
|
||||
id: 'spotifyUser123',
|
||||
};
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.spotify.com/v1/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await adapter.getUserFromAccessToken('validAccessToken');
|
||||
|
||||
expect(result).toEqual({ id: 'spotifyUser123' });
|
||||
});
|
||||
|
||||
it('should throw an error if the API request fails', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.spotify.com/v1/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken('invalidAccessToken')).toBeRejectedWithError(
|
||||
'Spotify API request failed.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAccessTokenFromCode', () => {
|
||||
it('should fetch access token successfully', async () => {
|
||||
const mockResponse = {
|
||||
access_token: 'validAccessToken',
|
||||
expires_in: 3600,
|
||||
refresh_token: 'refreshToken',
|
||||
};
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://accounts.spotify.com/api/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
redirect_uri: 'https://your-redirect-uri.com/callback',
|
||||
code_verifier: 'validCodeVerifier',
|
||||
};
|
||||
|
||||
const result = await adapter.getAccessTokenFromCode(authData);
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('should throw an error if authData is missing required fields', async () => {
|
||||
const authData = {
|
||||
redirect_uri: 'https://your-redirect-uri.com/callback',
|
||||
};
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
|
||||
'Spotify auth configuration authData.code and/or authData.redirect_uri and/or authData.code_verifier.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if the API request fails', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://accounts.spotify.com/api/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Bad Request',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'invalidCode',
|
||||
redirect_uri: 'https://your-redirect-uri.com/callback',
|
||||
code_verifier: 'invalidCodeVerifier',
|
||||
};
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
|
||||
'Spotify API request failed.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,120 +0,0 @@
|
||||
const TwitterAuthAdapter = require('../../../lib/Adapters/Auth/twitter').default;
|
||||
|
||||
describe('TwitterAuthAdapter', function () {
|
||||
let adapter;
|
||||
const validOptions = {
|
||||
consumer_key: 'validConsumerKey',
|
||||
consumer_secret: 'validConsumerSecret',
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new TwitterAuthAdapter.constructor();
|
||||
});
|
||||
|
||||
describe('Test configuration errors', function () {
|
||||
it('should throw an error when options are missing', function () {
|
||||
expect(() => adapter.validateOptions()).toThrowError('Twitter auth options are required.');
|
||||
});
|
||||
|
||||
it('should throw an error when consumer_key and consumer_secret are missing for secure auth', function () {
|
||||
const options = { enableInsecureAuth: false };
|
||||
expect(() => adapter.validateOptions(options)).toThrowError(
|
||||
'Consumer key and secret are required for secure Twitter auth.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should not throw an error when valid options are provided', function () {
|
||||
expect(() => adapter.validateOptions(validOptions)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Validate Insecure Auth', function () {
|
||||
it('should throw an error if oauth_token or oauth_token_secret are missing', async function () {
|
||||
const authData = { oauth_token: 'validToken' }; // Missing oauth_token_secret
|
||||
await expectAsync(adapter.validateInsecureAuth(authData, validOptions)).toBeRejectedWithError(
|
||||
'Twitter insecure auth requires oauth_token and oauth_token_secret.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should validate insecure auth successfully when data matches', async function () {
|
||||
spyOn(adapter, 'request').and.returnValue(
|
||||
Promise.resolve({
|
||||
json: () => Promise.resolve({ id: 'validUserId' }),
|
||||
})
|
||||
);
|
||||
|
||||
const authData = {
|
||||
id: 'validUserId',
|
||||
oauth_token: 'validToken',
|
||||
oauth_token_secret: 'validSecret',
|
||||
};
|
||||
await expectAsync(adapter.validateInsecureAuth(authData, validOptions)).toBeResolved();
|
||||
});
|
||||
|
||||
it('should throw an error when user ID does not match', async function () {
|
||||
spyOn(adapter, 'request').and.returnValue(
|
||||
Promise.resolve({
|
||||
json: () => Promise.resolve({ id: 'invalidUserId' }),
|
||||
})
|
||||
);
|
||||
|
||||
const authData = {
|
||||
id: 'validUserId',
|
||||
oauth_token: 'validToken',
|
||||
oauth_token_secret: 'validSecret',
|
||||
};
|
||||
await expectAsync(adapter.validateInsecureAuth(authData, validOptions)).toBeRejectedWithError(
|
||||
'Twitter auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('End-to-End Tests', function () {
|
||||
beforeEach(async function () {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
twitter: validOptions,
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
it('should authenticate user successfully using validateAuthData', async function () {
|
||||
spyOn(adapter, 'exchangeAccessToken').and.returnValue(
|
||||
Promise.resolve({ oauth_token: 'validToken', user_id: 'validUserId' })
|
||||
);
|
||||
|
||||
const authData = {
|
||||
oauth_token: 'validToken',
|
||||
oauth_verifier: 'validVerifier',
|
||||
};
|
||||
await expectAsync(adapter.validateAuthData(authData, validOptions)).toBeResolved();
|
||||
expect(authData.id).toBe('validUserId');
|
||||
expect(authData.auth_token).toBe('validToken');
|
||||
});
|
||||
|
||||
it('should handle multiple configurations and validate successfully', async function () {
|
||||
const authData = {
|
||||
consumer_key: 'validConsumerKey',
|
||||
oauth_token: 'validToken',
|
||||
oauth_token_secret: 'validSecret',
|
||||
};
|
||||
|
||||
const optionsArray = [
|
||||
{ consumer_key: 'invalidKey', consumer_secret: 'invalidSecret' },
|
||||
validOptions,
|
||||
];
|
||||
|
||||
const selectedOption = adapter.handleMultipleConfigurations(authData, optionsArray);
|
||||
expect(selectedOption).toEqual(validOptions);
|
||||
});
|
||||
|
||||
it('should throw an error when no matching configuration is found', function () {
|
||||
const authData = { consumer_key: 'missingKey' };
|
||||
const optionsArray = [validOptions];
|
||||
|
||||
expect(() => adapter.handleMultipleConfigurations(authData, optionsArray)).toThrowError(
|
||||
'Twitter auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,236 +0,0 @@
|
||||
const WeChatAdapter = require('../../../lib/Adapters/Auth/wechat').default;
|
||||
|
||||
describe('WeChatAdapter', function () {
|
||||
let adapter;
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new WeChatAdapter.constructor();
|
||||
});
|
||||
|
||||
describe('Test getUserFromAccessToken', function () {
|
||||
it('should fetch user successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/auth?access_token=validToken&openid=validOpenId',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ errcode: 0, id: 'validUserId' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const user = await adapter.getUserFromAccessToken('validToken', { id: 'validOpenId' });
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://api.weixin.qq.com/sns/auth?access_token=validToken&openid=validOpenId',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
expect(user).toEqual({ errcode: 0, id: 'validUserId' });
|
||||
});
|
||||
|
||||
it('should throw error for invalid response', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/auth?access_token=invalidToken&openid=undefined',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
json: () => Promise.resolve({ errcode: 40013, errmsg: 'Invalid token' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken('invalidToken', 'invalidOpenId')).toBeRejectedWith(
|
||||
jasmine.objectContaining({ message: 'WeChat auth is invalid for this user.' })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test getAccessTokenFromCode', function () {
|
||||
it('should fetch access token successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=validCode&grant_type=authorization_code',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validToken', errcode: 0 }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
adapter.validateOptions({ clientId: 'validAppId', clientSecret: 'validAppSecret' });
|
||||
const authData = { code: 'validCode' };
|
||||
const token = await adapter.getAccessTokenFromCode(authData);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=validCode&grant_type=authorization_code',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
expect(token).toEqual('validToken');
|
||||
});
|
||||
|
||||
it('should throw error for invalid response', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=invalidCode&grant_type=authorization_code',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
json: () => Promise.resolve({ errcode: 40029, errmsg: 'Invalid code' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
adapter.validateOptions({ clientId: 'validAppId', clientSecret: 'validAppSecret' });
|
||||
|
||||
const authData = { code: 'invalidCode' };
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWith(
|
||||
jasmine.objectContaining({ message: 'WeChat auth is invalid for this user.' })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('WeChatAdapter E2E Tests', function () {
|
||||
beforeEach(async () => {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
wechat: {
|
||||
clientId: 'validAppId',
|
||||
clientSecret: 'validAppSecret',
|
||||
enableInsecureAuth: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should authenticate user successfully using WeChatAdapter', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=validCode&grant_type=authorization_code',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validAccessToken', openid: 'user123', errcode: 0 }),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/auth?access_token=validAccessToken&openid=user123',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ errcode: 0, id: 'user123' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
|
||||
const user = await Parse.User.logInWith('wechat', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle invalid code error gracefully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=invalidCode&grant_type=authorization_code',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
json: () => Promise.resolve({ errcode: 40029, errmsg: 'Invalid code' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'invalidCode', redirect_uri: 'http://example.com/callback' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('wechat', { authData })).toBeRejectedWith(
|
||||
jasmine.objectContaining({ message: 'WeChat auth is invalid for this user.' })
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when fetching user data fails', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=validCode&grant_type=authorization_code',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validAccessToken', openid: 'user123', errcode: 0 }),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/auth?access_token=validAccessToken&openid=user123',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
json: () => Promise.resolve({ errcode: 40013, errmsg: 'Invalid token' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('wechat', { authData })).toBeRejectedWith(
|
||||
jasmine.objectContaining({ message: 'WeChat auth is invalid for this user.' })
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow insecure auth when enabled', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/auth?access_token=validAccessToken&openid=user123',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ errcode: 0, id: 'user123' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
wechat: {
|
||||
appId: 'validAppId',
|
||||
appSecret: 'validAppSecret',
|
||||
enableInsecureAuth: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const authData = { access_token: 'validAccessToken', id: 'user123' };
|
||||
const user = await Parse.User.logInWith('wechat', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should reject insecure auth when user id does not match', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/auth?access_token=validAccessToken&openid=incorrectUserId',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ errcode: 0, id: 'incorrectUser' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
wechat: {
|
||||
appId: 'validAppId',
|
||||
appSecret: 'validAppSecret',
|
||||
enableInsecureAuth: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const authData = { access_token: 'validAccessToken', id: 'incorrectUserId' };
|
||||
await expectAsync(Parse.User.logInWith('wechat', { authData })).toBeRejectedWith(
|
||||
jasmine.objectContaining({ message: 'WeChat auth is invalid for this user.' })
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,204 +0,0 @@
|
||||
const WeiboAdapter = require('../../../lib/Adapters/Auth/weibo').default;
|
||||
|
||||
describe('WeiboAdapter', function () {
|
||||
let adapter;
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new WeiboAdapter.constructor();
|
||||
});
|
||||
|
||||
describe('Test configuration errors', function () {
|
||||
it('should throw error if code or redirect_uri is missing', async function () {
|
||||
const invalidAuthData = [
|
||||
{},
|
||||
{ code: 'validCode' },
|
||||
{ redirect_uri: 'http://example.com/callback' },
|
||||
];
|
||||
|
||||
for (const authData of invalidAuthData) {
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWith(
|
||||
jasmine.objectContaining({
|
||||
message: 'Weibo auth requires code and redirect_uri to be sent.',
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test getUserFromAccessToken', function () {
|
||||
it('should fetch user successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weibo.com/oauth2/get_token_info',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ uid: 'validUserId' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { id: 'validUserId' };
|
||||
const user = await adapter.getUserFromAccessToken('validToken', authData);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://api.weibo.com/oauth2/get_token_info',
|
||||
jasmine.objectContaining({
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
})
|
||||
);
|
||||
expect(user).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
|
||||
it('should throw error for invalid response', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weibo.com/oauth2/get_token_info',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
json: () => Promise.resolve({}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { id: 'invalidUserId' };
|
||||
await expectAsync(adapter.getUserFromAccessToken('invalidToken', authData)).toBeRejectedWith(
|
||||
jasmine.objectContaining({
|
||||
message: 'Weibo auth is invalid for this user.',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test getAccessTokenFromCode', function () {
|
||||
it('should fetch access token successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weibo.com/oauth2/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validToken', uid: 'validUserId' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
|
||||
const token = await adapter.getAccessTokenFromCode(authData);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://api.weibo.com/oauth2/access_token',
|
||||
jasmine.objectContaining({
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
})
|
||||
);
|
||||
expect(token).toEqual('validToken');
|
||||
});
|
||||
|
||||
it('should throw error for invalid response', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weibo.com/oauth2/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
json: () => Promise.resolve({ errcode: 40029 }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'invalidCode', redirect_uri: 'http://example.com/callback' };
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWith(
|
||||
jasmine.objectContaining({
|
||||
message: 'Weibo auth is invalid for this user.',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('WeiboAdapter E2E Tests', function () {
|
||||
beforeEach(async () => {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
weibo: {
|
||||
clientId: 'validAppId',
|
||||
clientSecret: 'validAppSecret',
|
||||
},
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should authenticate user successfully using WeiboAdapter', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weibo.com/oauth2/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validAccessToken', uid: 'user123' }),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.weibo.com/oauth2/get_token_info',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ uid: 'user123' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
|
||||
const user = await Parse.User.logInWith('weibo', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle invalid code error gracefully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weibo.com/oauth2/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
json: () => Promise.resolve({ errcode: 40029 }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'invalidCode', redirect_uri: 'http://example.com/callback' };
|
||||
await expectAsync(Parse.User.logInWith('weibo', { authData })).toBeRejectedWith(
|
||||
jasmine.objectContaining({ message: 'Weibo auth is invalid for this user.' })
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when fetching user data fails', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weibo.com/oauth2/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validAccessToken', uid: 'user123' }),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.weibo.com/oauth2/get_token_info',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
json: () => Promise.resolve({}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
|
||||
await expectAsync(Parse.User.logInWith('weibo', { authData })).toBeRejectedWith(
|
||||
jasmine.objectContaining({ message: 'Weibo auth is invalid for this user.' })
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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.`
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -317,7 +317,7 @@ describe('AudiencesRouter', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it_id('af1111b5-3251-4b40-8f06-fb0fc624fa91')(it_exclude_dbs(['postgres']))('should support legacy parse.com audience fields', done => {
|
||||
it_exclude_dbs(['postgres'])('should support legacy parse.com audience fields', done => {
|
||||
const database = Config.get(Parse.applicationId).database.adapter.database;
|
||||
const now = new Date();
|
||||
Parse._request(
|
||||
@@ -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
-44
@@ -81,37 +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(),
|
||||
}
|
||||
);
|
||||
Parse.Server.cacheController.clear();
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
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 () => {
|
||||
@@ -233,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);
|
||||
});
|
||||
});
|
||||
|
||||
+1034
-504
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+71
-110
@@ -1,5 +1,5 @@
|
||||
'use strict';
|
||||
let commander;
|
||||
const commander = require('../lib/cli/utils/commander').default;
|
||||
const definitions = require('../lib/cli/definitions/parse-server').default;
|
||||
const liveQueryDefinitions = require('../lib/cli/definitions/parse-live-query-server').default;
|
||||
const path = require('path');
|
||||
@@ -28,12 +28,6 @@ const testDefinitions = {
|
||||
};
|
||||
|
||||
describe('commander additions', () => {
|
||||
beforeEach(() => {
|
||||
const command = require('../lib/cli/utils/commander').default;
|
||||
commander = new command.constructor();
|
||||
commander.storeOptionsAsProperties();
|
||||
commander.allowExcessArguments();
|
||||
});
|
||||
afterEach(done => {
|
||||
commander.options = [];
|
||||
delete commander.arg0;
|
||||
@@ -80,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',
|
||||
@@ -92,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 => {
|
||||
@@ -213,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) {
|
||||
@@ -254,83 +219,79 @@ describe('execution', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it_id('a0ab74b4-f805-4e03-b31d-b5cd59e64495')(it)('should start Parse Server', done => {
|
||||
const env = { ...process.env };
|
||||
env.NODE_OPTIONS = '--dns-result-order=ipv4first --trace-deprecation';
|
||||
childProcess = spawn(
|
||||
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_id('d7165081-b133-4cba-901b-19128ce41301')(it)('should start Parse Server with GraphQL', async done => {
|
||||
const env = { ...process.env };
|
||||
env.NODE_OPTIONS = '--dns-result-order=ipv4first --trace-deprecation';
|
||||
childProcess = spawn(
|
||||
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_id('2769cdb4-ce8a-484d-8a91-635b5894ba7e')(it)('should start Parse Server with GraphQL and Playground', async done => {
|
||||
const env = { ...process.env };
|
||||
env.NODE_OPTIONS = '--dns-result-order=ipv4first --trace-deprecation';
|
||||
childProcess = spawn(
|
||||
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_id('23caddd7-bfea-4869-8bd4-0f2cd283c8bd')(it)('can start Parse Server with auth via CLI', done => {
|
||||
const env = { ...process.env };
|
||||
env.NODE_OPTIONS = '--dns-result-order=ipv4first --trace-deprecation';
|
||||
childProcess = spawn(
|
||||
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());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,9 +6,6 @@ const validatorFail = () => {
|
||||
const validatorSuccess = () => {
|
||||
return true;
|
||||
};
|
||||
function testConfig() {
|
||||
return Parse.Config.save({ internal: 'i', string: 's', number: 12 }, { internal: true });
|
||||
}
|
||||
|
||||
describe('cloud validator', () => {
|
||||
it('complete validator', async done => {
|
||||
@@ -197,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',
|
||||
@@ -734,38 +752,6 @@ describe('cloud validator', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it_id('893eec0c-41bd-4adf-8f0a-306087ad8d61')(it)('basic beforeSave Parse.Config skipWithMasterKey', async () => {
|
||||
Parse.Cloud.beforeSave(
|
||||
Parse.Config,
|
||||
() => {
|
||||
throw 'beforeSaveFile should have resolved using master key.';
|
||||
},
|
||||
{
|
||||
skipWithMasterKey: true,
|
||||
}
|
||||
);
|
||||
const config = await testConfig();
|
||||
expect(config.get('internal')).toBe('i');
|
||||
expect(config.get('string')).toBe('s');
|
||||
expect(config.get('number')).toBe(12);
|
||||
});
|
||||
|
||||
it_id('91e739a4-6a38-405c-8f83-f36d48220734')(it)('basic afterSave Parse.Config skipWithMasterKey', async () => {
|
||||
Parse.Cloud.afterSave(
|
||||
Parse.Config,
|
||||
() => {
|
||||
throw 'beforeSaveFile should have resolved using master key.';
|
||||
},
|
||||
{
|
||||
skipWithMasterKey: true,
|
||||
}
|
||||
);
|
||||
const config = await testConfig();
|
||||
expect(config.get('internal')).toBe('i');
|
||||
expect(config.get('string')).toBe('s');
|
||||
expect(config.get('number')).toBe(12);
|
||||
});
|
||||
|
||||
it('beforeSave validateMasterKey and skipWithMasterKey fail', async function (done) {
|
||||
Parse.Cloud.beforeSave(
|
||||
'BeforeSave',
|
||||
@@ -1476,7 +1462,7 @@ describe('cloud validator', () => {
|
||||
});
|
||||
|
||||
it('validate afterSaveFile fail', async done => {
|
||||
Parse.Cloud.afterSave(Parse.File, () => {}, validatorFail);
|
||||
Parse.Cloud.beforeSave(Parse.File, () => {}, validatorFail);
|
||||
try {
|
||||
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
|
||||
await file.save({ useMasterKey: true });
|
||||
@@ -1531,42 +1517,6 @@ describe('cloud validator', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it_id('32ca1a99-7f2b-429d-a7cf-62b6661d0af6')(it)('validate beforeSave Parse.Config', async () => {
|
||||
Parse.Cloud.beforeSave(Parse.Config, () => {}, validatorSuccess);
|
||||
const config = await testConfig();
|
||||
expect(config.get('internal')).toBe('i');
|
||||
expect(config.get('string')).toBe('s');
|
||||
expect(config.get('number')).toBe(12);
|
||||
});
|
||||
|
||||
it_id('c84d11e7-d09c-4843-ad98-f671511bf612')(it)('validate beforeSave Parse.Config fail', async () => {
|
||||
Parse.Cloud.beforeSave(Parse.Config, () => {}, validatorFail);
|
||||
try {
|
||||
await testConfig();
|
||||
fail('cloud function should have failed.');
|
||||
} catch (e) {
|
||||
expect(e.code).toBe(Parse.Error.VALIDATION_ERROR);
|
||||
}
|
||||
});
|
||||
|
||||
it_id('b18b9a6a-0e35-4b60-9771-30f53501df3c')(it)('validate afterSave Parse.Config', async () => {
|
||||
Parse.Cloud.afterSave(Parse.Config, () => {}, validatorSuccess);
|
||||
const config = await testConfig();
|
||||
expect(config.get('internal')).toBe('i');
|
||||
expect(config.get('string')).toBe('s');
|
||||
expect(config.get('number')).toBe(12);
|
||||
});
|
||||
|
||||
it_id('ef761222-1758-4614-b984-da84d73fc10c')(it)('validate afterSave Parse.Config fail', async () => {
|
||||
Parse.Cloud.afterSave(Parse.Config, () => {}, validatorFail);
|
||||
try {
|
||||
await testConfig();
|
||||
fail('cloud function should have failed.');
|
||||
} catch (e) {
|
||||
expect(e.code).toBe(Parse.Error.VALIDATION_ERROR);
|
||||
}
|
||||
});
|
||||
|
||||
it('Should have validator', async done => {
|
||||
Parse.Cloud.define(
|
||||
'myFunction',
|
||||
@@ -1679,7 +1629,7 @@ describe('cloud validator', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('Logs on multiple invalid configs', () => {
|
||||
it('Logs on invalid config', () => {
|
||||
const fields = [
|
||||
{
|
||||
field: 'otherKey',
|
||||
|
||||
+113
-999
File diff suppressed because it is too large
Load Diff
+13
-148
@@ -15,17 +15,10 @@ 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')
|
||||
.catch(() => { })
|
||||
.catch(() => {})
|
||||
.then(loggedInUser => (user = loggedInUser))
|
||||
.then(() => Parse.User.logIn(user.get('username'), 'abc'));
|
||||
})
|
||||
@@ -37,7 +30,7 @@ describe('Cloud Code Logger', () => {
|
||||
// Note that helpers takes care of logout.
|
||||
// see helpers.js:afterEach
|
||||
|
||||
it_id('02d53b97-3ec7-46fb-abb6-176fd6e85590')(it)('should expose log to functions', () => {
|
||||
it('should expose log to functions', () => {
|
||||
const spy = spyOn(Config.get('test').loggerController, 'log').and.callThrough();
|
||||
Parse.Cloud.define('loggerTest', req => {
|
||||
req.log.info('logTest', 'info log', { info: 'some log' });
|
||||
@@ -67,7 +60,7 @@ describe('Cloud Code Logger', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_id('768412f5-d32f-4134-89a6-08949781a6c0')(it)('trigger should obfuscate password', done => {
|
||||
it('trigger should obfuscate password', done => {
|
||||
Parse.Cloud.beforeSave(Parse.User, req => {
|
||||
return req.object;
|
||||
});
|
||||
@@ -82,7 +75,7 @@ describe('Cloud Code Logger', () => {
|
||||
.then(null, e => done.fail(e));
|
||||
});
|
||||
|
||||
it_id('3c394047-272e-4728-9d02-9eaa660d2ed2')(it)('should expose log to trigger', done => {
|
||||
it('should expose log to trigger', done => {
|
||||
Parse.Cloud.beforeSave('MyObject', req => {
|
||||
req.log.info('beforeSave MyObject', 'info log', { info: 'some log' });
|
||||
req.log.error('beforeSave MyObject', 'error log', {
|
||||
@@ -120,7 +113,7 @@ describe('Cloud Code Logger', () => {
|
||||
expect(truncatedString.length).toBe(1015); // truncate length + the string '... (truncated)'
|
||||
});
|
||||
|
||||
it_id('4a009b1f-9203-49ca-8d48-5b45f4eedbdf')(it)('should truncate input and result of long lines', done => {
|
||||
it('should truncate input and result of long lines', done => {
|
||||
const longString = fs.readFileSync(loremFile, 'utf8');
|
||||
Parse.Cloud.define('aFunction', req => {
|
||||
return req.params;
|
||||
@@ -138,8 +131,8 @@ describe('Cloud Code Logger', () => {
|
||||
.then(null, e => done.fail(e));
|
||||
});
|
||||
|
||||
it_id('9857e15d-bb18-478d-8a67-fdaad3e89565')(it)('should log an afterSave', done => {
|
||||
Parse.Cloud.afterSave('MyObject', () => { });
|
||||
it('should log an afterSave', done => {
|
||||
Parse.Cloud.afterSave('MyObject', () => {});
|
||||
new Parse.Object('MyObject')
|
||||
.save()
|
||||
.then(() => {
|
||||
@@ -151,7 +144,7 @@ describe('Cloud Code Logger', () => {
|
||||
.then(null, e => done.fail(e));
|
||||
});
|
||||
|
||||
it_id('ec13a296-f8b1-4fc6-985a-3593462edd9c')(it)('should log a denied beforeSave', done => {
|
||||
it('should log a denied beforeSave', done => {
|
||||
Parse.Cloud.beforeSave('MyObject', () => {
|
||||
throw 'uh oh!';
|
||||
});
|
||||
@@ -174,7 +167,7 @@ describe('Cloud Code Logger', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_id('3e0caa45-60d6-41af-829a-fd389710c132')(it)('should log cloud function success', done => {
|
||||
it('should log cloud function success', done => {
|
||||
Parse.Cloud.define('aFunction', () => {
|
||||
return 'it worked!';
|
||||
});
|
||||
@@ -189,89 +182,13 @@ describe('Cloud Code Logger', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_id('8088de8a-7cba-4035-8b05-4a903307e674')(it)('should log cloud function execution using the custom log level', async () => {
|
||||
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');
|
||||
});
|
||||
|
||||
Parse.Cloud._removeAllHooks();
|
||||
await reconfigureServer({
|
||||
silent: true,
|
||||
logLevels: {
|
||||
cloudFunctionSuccess: 'warn',
|
||||
cloudFunctionError: 'info',
|
||||
},
|
||||
});
|
||||
|
||||
Parse.Cloud.define('bFunction', () => {
|
||||
throw new Error('Failed');
|
||||
});
|
||||
|
||||
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');
|
||||
}
|
||||
});
|
||||
|
||||
it('should log cloud function triggers using the custom log level', async () => {
|
||||
const execTest = async (logLevel, triggerBeforeSuccess, triggerAfter) => {
|
||||
Parse.Cloud._removeAllHooks();
|
||||
await reconfigureServer({
|
||||
silent: true,
|
||||
logLevel,
|
||||
logLevels: {
|
||||
triggerAfter,
|
||||
triggerBeforeSuccess,
|
||||
},
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
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_id('97e0eafa-cde6-4a9a-9e53-7db98bacbc62')(it)('should log cloud function failure', done => {
|
||||
it('should log cloud function failure', done => {
|
||||
Parse.Cloud.define('aFunction', () => {
|
||||
throw 'it failed!';
|
||||
});
|
||||
|
||||
Parse.Cloud.run('aFunction', { foo: 'bar' })
|
||||
.catch(() => { })
|
||||
.catch(() => {})
|
||||
.then(() => {
|
||||
const logs = spy.calls.all().reverse();
|
||||
expect(logs[0].args[1]).toBe('Parse error: ');
|
||||
@@ -292,7 +209,6 @@ describe('Cloud Code Logger', () => {
|
||||
});
|
||||
|
||||
xit('should log a changed beforeSave indicating a change', done => {
|
||||
pending('needs more work.....');
|
||||
const logController = new LoggerController(new WinstonLoggerAdapter());
|
||||
|
||||
Parse.Cloud.beforeSave('MyObject', req => {
|
||||
@@ -315,9 +231,9 @@ describe('Cloud Code Logger', () => {
|
||||
done();
|
||||
})
|
||||
.then(null, e => done.fail(JSON.stringify(e)));
|
||||
});
|
||||
}).pend('needs more work.....');
|
||||
|
||||
it_id('b86e8168-8370-4730-a4ba-24ca3016ad66')(it)('cloud function should obfuscate password', done => {
|
||||
it('cloud function should obfuscate password', done => {
|
||||
Parse.Cloud.define('testFunction', () => {
|
||||
return 'verify code success';
|
||||
});
|
||||
@@ -347,55 +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 () => {
|
||||
Parse.Cloud._removeAllHooks();
|
||||
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 () => {
|
||||
Parse.Cloud._removeAllHooks();
|
||||
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,475 +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 },
|
||||
_email_verify_token: { _email_verify_token: 1 },
|
||||
_perishable_token: { _perishable_token: 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 },
|
||||
_email_verify_token: { _email_verify_token: 1 },
|
||||
_perishable_token: { _perishable_token: 1 },
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
it_only_db('mongo')(
|
||||
'should use _email_verify_token index in email verification',
|
||||
async () => {
|
||||
const TestUtils = require('../lib/TestUtils');
|
||||
let emailVerificationLink;
|
||||
const emailSentPromise = TestUtils.resolvingPromise();
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: options => {
|
||||
emailVerificationLink = options.link;
|
||||
emailSentPromise.resolve();
|
||||
},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {},
|
||||
};
|
||||
await reconfigureServer({
|
||||
databaseURI: 'mongodb://localhost:27017/testEmailVerifyTokenIndexStats',
|
||||
databaseAdapter: undefined,
|
||||
appName: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
});
|
||||
|
||||
// Create a user to trigger email verification
|
||||
const user = new Parse.User();
|
||||
user.setUsername('statsuser');
|
||||
user.setPassword('password');
|
||||
user.set('email', 'stats@example.com');
|
||||
await user.signUp();
|
||||
await emailSentPromise;
|
||||
|
||||
// Get index stats before the query
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const collection = await config.database.adapter._adaptiveCollection('_User');
|
||||
const statsBefore = await collection._mongoCollection.aggregate([
|
||||
{ $indexStats: {} },
|
||||
]).toArray();
|
||||
const emailVerifyIndexBefore = statsBefore.find(
|
||||
stat => stat.name === '_email_verify_token'
|
||||
);
|
||||
const accessesBefore = emailVerifyIndexBefore?.accesses?.ops || 0;
|
||||
|
||||
// Perform email verification (this should use the index)
|
||||
const request = require('../lib/request');
|
||||
await request({
|
||||
url: emailVerificationLink,
|
||||
followRedirects: false,
|
||||
});
|
||||
|
||||
// Get index stats after the query
|
||||
const statsAfter = await collection._mongoCollection.aggregate([
|
||||
{ $indexStats: {} },
|
||||
]).toArray();
|
||||
const emailVerifyIndexAfter = statsAfter.find(
|
||||
stat => stat.name === '_email_verify_token'
|
||||
);
|
||||
const accessesAfter = emailVerifyIndexAfter?.accesses?.ops || 0;
|
||||
|
||||
// Verify the index was actually used
|
||||
expect(accessesAfter).toBeGreaterThan(accessesBefore);
|
||||
expect(emailVerifyIndexAfter).toBeDefined();
|
||||
|
||||
// Verify email verification succeeded
|
||||
await user.fetch();
|
||||
expect(user.get('emailVerified')).toBe(true);
|
||||
}
|
||||
);
|
||||
|
||||
it_only_db('mongo')(
|
||||
'should use _perishable_token index in password reset',
|
||||
async () => {
|
||||
const TestUtils = require('../lib/TestUtils');
|
||||
let passwordResetLink;
|
||||
const emailSentPromise = TestUtils.resolvingPromise();
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: options => {
|
||||
passwordResetLink = options.link;
|
||||
emailSentPromise.resolve();
|
||||
},
|
||||
sendMail: () => {},
|
||||
};
|
||||
await reconfigureServer({
|
||||
databaseURI: 'mongodb://localhost:27017/testPerishableTokenIndexStats',
|
||||
databaseAdapter: undefined,
|
||||
appName: 'test',
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
});
|
||||
|
||||
// Create a user
|
||||
const user = new Parse.User();
|
||||
user.setUsername('statsuser2');
|
||||
user.setPassword('oldpassword');
|
||||
user.set('email', 'stats2@example.com');
|
||||
await user.signUp();
|
||||
|
||||
// Request password reset
|
||||
await Parse.User.requestPasswordReset('stats2@example.com');
|
||||
await emailSentPromise;
|
||||
|
||||
const url = new URL(passwordResetLink);
|
||||
const token = url.searchParams.get('token');
|
||||
|
||||
// Get index stats before the query
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const collection = await config.database.adapter._adaptiveCollection('_User');
|
||||
const statsBefore = await collection._mongoCollection.aggregate([
|
||||
{ $indexStats: {} },
|
||||
]).toArray();
|
||||
const perishableTokenIndexBefore = statsBefore.find(
|
||||
stat => stat.name === '_perishable_token'
|
||||
);
|
||||
const accessesBefore = perishableTokenIndexBefore?.accesses?.ops || 0;
|
||||
|
||||
// Perform password reset (this should use the index)
|
||||
const request = require('../lib/request');
|
||||
await request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: { new_password: 'newpassword', token, username: 'statsuser2' },
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
followRedirects: false,
|
||||
});
|
||||
|
||||
// Get index stats after the query
|
||||
const statsAfter = await collection._mongoCollection.aggregate([
|
||||
{ $indexStats: {} },
|
||||
]).toArray();
|
||||
const perishableTokenIndexAfter = statsAfter.find(
|
||||
stat => stat.name === '_perishable_token'
|
||||
);
|
||||
const accessesAfter = perishableTokenIndexAfter?.accesses?.ops || 0;
|
||||
|
||||
// Verify the index was actually used
|
||||
expect(accessesAfter).toBeGreaterThan(accessesBefore);
|
||||
expect(perishableTokenIndexAfter).toBeDefined();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update with validateOnly', () => {
|
||||
const mockStorageAdapter = {
|
||||
findOneAndUpdate: () => Promise.resolve({}),
|
||||
find: () => Promise.resolve([{ objectId: 'test123', testField: 'initialValue' }]),
|
||||
watch: () => Promise.resolve(),
|
||||
getAllClasses: () =>
|
||||
Promise.resolve([
|
||||
{
|
||||
className: 'TestObject',
|
||||
fields: { testField: 'String' },
|
||||
indexes: {},
|
||||
classLevelPermissions: { protectedFields: {} },
|
||||
},
|
||||
]),
|
||||
};
|
||||
|
||||
it('should use primary readPreference when validateOnly is true', async () => {
|
||||
const databaseController = new DatabaseController(mockStorageAdapter, {});
|
||||
const findSpy = spyOn(mockStorageAdapter, 'find').and.callThrough();
|
||||
const findOneAndUpdateSpy = spyOn(mockStorageAdapter, 'findOneAndUpdate').and.callThrough();
|
||||
|
||||
try {
|
||||
// Call update with validateOnly: true (same as RestWrite.runBeforeSaveTrigger)
|
||||
await databaseController.update(
|
||||
'TestObject',
|
||||
{ objectId: 'test123' },
|
||||
{ testField: 'newValue' },
|
||||
{},
|
||||
true, // skipSanitization: true (matches RestWrite behavior)
|
||||
true // validateOnly: true
|
||||
);
|
||||
} catch (error) {
|
||||
// validateOnly may throw, but we're checking the find call options
|
||||
}
|
||||
|
||||
// Verify that find was called with primary readPreference
|
||||
expect(findSpy).toHaveBeenCalled();
|
||||
const findCall = findSpy.calls.mostRecent();
|
||||
expect(findCall.args[3]).toEqual({ readPreference: 'primary' }); // options parameter
|
||||
|
||||
// Verify that findOneAndUpdate was NOT called (only validation, no actual update)
|
||||
expect(findOneAndUpdateSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not use primary readPreference when validateOnly is false', async () => {
|
||||
const databaseController = new DatabaseController(mockStorageAdapter, {});
|
||||
const findSpy = spyOn(mockStorageAdapter, 'find').and.callThrough();
|
||||
const findOneAndUpdateSpy = spyOn(mockStorageAdapter, 'findOneAndUpdate').and.callThrough();
|
||||
|
||||
try {
|
||||
// Call update with validateOnly: false
|
||||
await databaseController.update(
|
||||
'TestObject',
|
||||
{ objectId: 'test123' },
|
||||
{ testField: 'newValue' },
|
||||
{},
|
||||
false, // skipSanitization
|
||||
false // validateOnly
|
||||
);
|
||||
} catch (error) {
|
||||
// May throw for other reasons, but we're checking the call pattern
|
||||
}
|
||||
|
||||
// When validateOnly is false, find should not be called for validation
|
||||
// Instead, findOneAndUpdate should be called
|
||||
expect(findSpy).not.toHaveBeenCalled();
|
||||
expect(findOneAndUpdateSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function buildCLP(pointerNames) {
|
||||
|
||||
@@ -371,7 +371,7 @@ describe('DefinedSchemas', () => {
|
||||
expect(schema.indexes).toEqual(indexes);
|
||||
});
|
||||
|
||||
it('should delete unknown indexes when keepUnknownIndexes is not set', async () => {
|
||||
it('should delete removed indexes', async () => {
|
||||
const server = await reconfigureServer();
|
||||
|
||||
let indexes = { complex: { createdAt: 1, updatedAt: 1 } };
|
||||
@@ -393,53 +393,6 @@ describe('DefinedSchemas', () => {
|
||||
cleanUpIndexes(schema);
|
||||
expect(schema.indexes).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should delete unknown indexes when keepUnknownIndexes is set to false', async () => {
|
||||
const server = await reconfigureServer();
|
||||
|
||||
let indexes = { complex: { createdAt: 1, updatedAt: 1 } };
|
||||
|
||||
let schemas = { definitions: [{ className: 'Test', indexes }], keepUnknownIndexes: false };
|
||||
await new DefinedSchemas(schemas, server.config).execute();
|
||||
|
||||
indexes = {};
|
||||
schemas = { definitions: [{ className: 'Test', indexes }], keepUnknownIndexes: false };
|
||||
// Change indexes
|
||||
await new DefinedSchemas(schemas, server.config).execute();
|
||||
let schema = await new Parse.Schema('Test').get();
|
||||
cleanUpIndexes(schema);
|
||||
expect(schema.indexes).toBeUndefined();
|
||||
|
||||
// Update
|
||||
await new DefinedSchemas(schemas, server.config).execute();
|
||||
schema = await new Parse.Schema('Test').get();
|
||||
cleanUpIndexes(schema);
|
||||
expect(schema.indexes).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not delete unknown indexes when keepUnknownIndexes is set to true', async () => {
|
||||
const server = await reconfigureServer();
|
||||
|
||||
const indexes = { complex: { createdAt: 1, updatedAt: 1 } };
|
||||
|
||||
let schemas = { definitions: [{ className: 'Test', indexes }], keepUnknownIndexes: true };
|
||||
await new DefinedSchemas(schemas, server.config).execute();
|
||||
|
||||
schemas = { definitions: [{ className: 'Test', indexes: {} }], keepUnknownIndexes: true };
|
||||
|
||||
// Change indexes
|
||||
await new DefinedSchemas(schemas, server.config).execute();
|
||||
let schema = await new Parse.Schema('Test').get();
|
||||
cleanUpIndexes(schema);
|
||||
expect(schema.indexes).toEqual({ complex: { createdAt: 1, updatedAt: 1 } });
|
||||
|
||||
// Update
|
||||
await new DefinedSchemas(schemas, server.config).execute();
|
||||
schema = await new Parse.Schema('Test').get();
|
||||
cleanUpIndexes(schema);
|
||||
expect(schema.indexes).toEqual(indexes);
|
||||
});
|
||||
|
||||
xit('should keep protected indexes', async () => {
|
||||
const server = await reconfigureServer();
|
||||
|
||||
@@ -601,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' }] },
|
||||
});
|
||||
@@ -678,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;
|
||||
});
|
||||
|
||||
@@ -689,8 +642,7 @@ describe('DefinedSchemas', () => {
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith(`Failed to run migrations: ${error.toString()}`);
|
||||
});
|
||||
|
||||
it_id('a18bf4f2-25c8-4de3-b986-19cb1ab163b8')(it)('should perform migration in parallel without failing', async () => {
|
||||
it('should perform migration in parallel without failing', async () => {
|
||||
const server = await reconfigureServer();
|
||||
const logger = require('../lib/logger').logger;
|
||||
spyOn(logger, 'error').and.callThrough();
|
||||
@@ -725,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();
|
||||
});
|
||||
});
|
||||
|
||||
+667
-818
File diff suppressed because it is too large
Load Diff
@@ -3,9 +3,11 @@ const request = require('../lib/request');
|
||||
describe('Enable express error handler', () => {
|
||||
it('should call the default handler in case of error, like updating a non existing object', async done => {
|
||||
spyOn(console, 'error');
|
||||
const parseServer = await reconfigureServer({
|
||||
enableExpressErrorHandler: true,
|
||||
});
|
||||
const parseServer = await reconfigureServer(
|
||||
Object.assign({}, defaultConfiguration, {
|
||||
enableExpressErrorHandler: true,
|
||||
})
|
||||
);
|
||||
parseServer.app.use(function (err, req, res, next) {
|
||||
expect(err.message).toBe('Object not found.');
|
||||
next(err);
|
||||
|
||||
@@ -21,14 +21,11 @@ const mockAdapter = {
|
||||
|
||||
// Small additional tests to improve overall coverage
|
||||
describe('FilesController', () => {
|
||||
it('should properly expand objects with sync getFileLocation', async () => {
|
||||
it('should properly expand objects', done => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
gridFSAdapter.getFileLocation = (config, filename) => {
|
||||
return config.mount + '/files/' + config.applicationId + '/' + encodeURIComponent(filename);
|
||||
}
|
||||
const filesController = new FilesController(gridFSAdapter);
|
||||
const result = await filesController.expandFilesInObject(config, function () { });
|
||||
const result = filesController.expandFilesInObject(config, function () {});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
|
||||
@@ -40,69 +37,12 @@ describe('FilesController', () => {
|
||||
const anObject = {
|
||||
aFile: fullFile,
|
||||
};
|
||||
await filesController.expandFilesInObject(config, anObject);
|
||||
filesController.expandFilesInObject(config, anObject);
|
||||
expect(anObject.aFile.url).toEqual('http://an.url');
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should properly expand objects with async getFileLocation', async () => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
gridFSAdapter.getFileLocation = async (config, filename) => {
|
||||
await Promise.resolve();
|
||||
return config.mount + '/files/' + config.applicationId + '/' + encodeURIComponent(filename);
|
||||
}
|
||||
const filesController = new FilesController(gridFSAdapter);
|
||||
const result = await filesController.expandFilesInObject(config, function () { });
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
|
||||
const fullFile = {
|
||||
type: '__type',
|
||||
url: 'http://an.url',
|
||||
};
|
||||
|
||||
const anObject = {
|
||||
aFile: fullFile,
|
||||
};
|
||||
await filesController.expandFilesInObject(config, anObject);
|
||||
expect(anObject.aFile.url).toEqual('http://an.url');
|
||||
});
|
||||
|
||||
it('should call getFileLocation when config.fileKey is undefined', async () => {
|
||||
const config = {};
|
||||
const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
|
||||
const fullFile = {
|
||||
name: 'mock-name',
|
||||
__type: 'File',
|
||||
};
|
||||
gridFSAdapter.getFileLocation = jasmine.createSpy('getFileLocation').and.returnValue(Promise.resolve('mock-url'));
|
||||
const filesController = new FilesController(gridFSAdapter);
|
||||
|
||||
const anObject = { aFile: fullFile };
|
||||
await filesController.expandFilesInObject(config, anObject);
|
||||
expect(gridFSAdapter.getFileLocation).toHaveBeenCalledWith(config, fullFile.name);
|
||||
expect(anObject.aFile.url).toEqual('mock-url');
|
||||
});
|
||||
|
||||
it('should call getFileLocation when config.fileKey is defined', async () => {
|
||||
const config = { fileKey: 'mock-key' };
|
||||
const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
|
||||
const fullFile = {
|
||||
name: 'mock-name',
|
||||
__type: 'File',
|
||||
};
|
||||
gridFSAdapter.getFileLocation = jasmine.createSpy('getFileLocation').and.returnValue(Promise.resolve('mock-url'));
|
||||
const filesController = new FilesController(gridFSAdapter);
|
||||
|
||||
const anObject = { aFile: fullFile };
|
||||
await filesController.expandFilesInObject(config, anObject);
|
||||
expect(gridFSAdapter.getFileLocation).toHaveBeenCalledWith(config, fullFile.name);
|
||||
expect(anObject.aFile.url).toEqual('mock-url');
|
||||
});
|
||||
|
||||
|
||||
it_only_db('mongo')('should pass databaseOptions to GridFSBucketAdapter', async () => {
|
||||
await reconfigureServer({
|
||||
databaseURI: 'mongodb://localhost:27017/parse',
|
||||
@@ -115,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 => {
|
||||
@@ -161,7 +99,7 @@ describe('FilesController', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should add a unique hash to the file name when the preserveFileName option is false', async () => {
|
||||
it('should add a unique hash to the file name when the preserveFileName option is false', done => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
spyOn(gridFSAdapter, 'createFile');
|
||||
@@ -172,15 +110,17 @@ describe('FilesController', () => {
|
||||
preserveFileName: false,
|
||||
});
|
||||
|
||||
await filesController.createFile(config, fileName);
|
||||
filesController.createFile(config, fileName);
|
||||
|
||||
expect(gridFSAdapter.createFile).toHaveBeenCalledTimes(1);
|
||||
expect(gridFSAdapter.createFile.calls.mostRecent().args[0]).toMatch(
|
||||
`^.{32}_${regexEscapedFileName}$`
|
||||
);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should not add a unique hash to the file name when the preserveFileName option is true', async () => {
|
||||
it('should not add a unique hash to the file name when the preserveFileName option is true', done => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
spyOn(gridFSAdapter, 'createFile');
|
||||
@@ -190,10 +130,12 @@ describe('FilesController', () => {
|
||||
preserveFileName: true,
|
||||
});
|
||||
|
||||
await filesController.createFile(config, fileName);
|
||||
filesController.createFile(config, fileName);
|
||||
|
||||
expect(gridFSAdapter.createFile).toHaveBeenCalledTimes(1);
|
||||
expect(gridFSAdapter.createFile.calls.mostRecent().args[0]).toEqual(fileName);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should handle adapter without getMetadata', async () => {
|
||||
|
||||
@@ -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(
|
||||
@@ -421,14 +405,6 @@ describe_only_db('mongo')('GridFSBucket', () => {
|
||||
expect(gfsResult.toString('utf8')).toBe(twoMegabytesFile);
|
||||
});
|
||||
|
||||
it('properly upload a file when disableIndexFieldValidation exist in databaseOptions', async () => {
|
||||
const gfsAdapter = new GridFSBucketAdapter(databaseURI, { disableIndexFieldValidation: true });
|
||||
const twoMegabytesFile = randomString(2048 * 1024);
|
||||
await gfsAdapter.createFile('myFileName', twoMegabytesFile);
|
||||
const gfsResult = await gfsAdapter.getFileData('myFileName');
|
||||
expect(gfsResult.toString('utf8')).toBe(twoMegabytesFile);
|
||||
});
|
||||
|
||||
it('properly deletes a file from GridFS', async () => {
|
||||
const gfsAdapter = new GridFSBucketAdapter(databaseURI);
|
||||
await gfsAdapter.createFile('myFileName', 'a simple file');
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
'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');
|
||||
|
||||
const port = 13371;
|
||||
@@ -9,7 +10,7 @@ const httpRequestServer = `http://localhost:${port}`;
|
||||
|
||||
function startServer(done) {
|
||||
const app = express();
|
||||
app.use(express.json({ type: '*/*' }));
|
||||
app.use(bodyParser.json({ type: '*/*' }));
|
||||
app.get('/hello', function (req, res) {
|
||||
res.json({ response: 'OK' });
|
||||
});
|
||||
|
||||
@@ -45,12 +45,12 @@ describe('Idempotency', () => {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
afterAll(() => {
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = process.env.PARSE_SERVER_TEST_TIMEOUT || 10000;
|
||||
});
|
||||
|
||||
// Tests
|
||||
it_id('e25955fd-92eb-4b22-b8b7-38980e5cb223')(it)('should enforce idempotency for cloud code function', async () => {
|
||||
it('should enforce idempotency for cloud code function', async () => {
|
||||
let counter = 0;
|
||||
Parse.Cloud.define('myFunction', () => {
|
||||
counter++;
|
||||
@@ -73,7 +73,7 @@ describe('Idempotency', () => {
|
||||
expect(counter).toBe(1);
|
||||
});
|
||||
|
||||
it_id('be2fbe16-8178-485e-9a12-6fb541096480')(it)('should delete request entry after TTL', async () => {
|
||||
it('should delete request entry after TTL', async () => {
|
||||
let counter = 0;
|
||||
Parse.Cloud.define('myFunction', () => {
|
||||
counter++;
|
||||
@@ -123,7 +123,7 @@ describe('Idempotency', () => {
|
||||
}
|
||||
);
|
||||
|
||||
it_id('e976d0cc-a57f-45d4-9472-b9b052db6490')(it)('should enforce idempotency for cloud code jobs', async () => {
|
||||
it('should enforce idempotency for cloud code jobs', async () => {
|
||||
let counter = 0;
|
||||
Parse.Cloud.job('myJob', () => {
|
||||
counter++;
|
||||
@@ -145,7 +145,7 @@ describe('Idempotency', () => {
|
||||
expect(counter).toBe(1);
|
||||
});
|
||||
|
||||
it_id('7c84a3d4-e1b6-4a0d-99f1-af3cf1a6b3d8')(it)('should enforce idempotency for class object creation', async () => {
|
||||
it('should enforce idempotency for class object creation', async () => {
|
||||
let counter = 0;
|
||||
Parse.Cloud.afterSave('MyClass', () => {
|
||||
counter++;
|
||||
@@ -167,7 +167,7 @@ describe('Idempotency', () => {
|
||||
expect(counter).toBe(1);
|
||||
});
|
||||
|
||||
it_id('a030f2dd-5d21-46ac-b53d-9d714f35d72a')(it)('should enforce idempotency for user object creation', async () => {
|
||||
it('should enforce idempotency for user object creation', async () => {
|
||||
let counter = 0;
|
||||
Parse.Cloud.afterSave('_User', () => {
|
||||
counter++;
|
||||
@@ -193,7 +193,7 @@ describe('Idempotency', () => {
|
||||
expect(counter).toBe(1);
|
||||
});
|
||||
|
||||
it_id('064c469b-091c-4ba9-9043-be461f26a3eb')(it)('should enforce idempotency for installation object creation', async () => {
|
||||
it('should enforce idempotency for installation object creation', async () => {
|
||||
let counter = 0;
|
||||
Parse.Cloud.afterSave('_Installation', () => {
|
||||
counter++;
|
||||
@@ -219,7 +219,7 @@ describe('Idempotency', () => {
|
||||
expect(counter).toBe(1);
|
||||
});
|
||||
|
||||
it_id('f11670b6-fa9c-4f21-a268-ae4b6bbff7fd')(it)('should not interfere with calls of different request ID', async () => {
|
||||
it('should not interfere with calls of different request ID', async () => {
|
||||
let counter = 0;
|
||||
Parse.Cloud.afterSave('MyClass', () => {
|
||||
counter++;
|
||||
@@ -240,7 +240,7 @@ describe('Idempotency', () => {
|
||||
expect(counter).toBe(100);
|
||||
});
|
||||
|
||||
it_id('0ecd2cd2-dafb-4a2b-bb2b-9ad4c9aca777')(it)('should re-throw any other error unchanged when writing request entry fails for any other reason', async () => {
|
||||
it('should re-throw any other error unchanged when writing request entry fails for any other reason', async () => {
|
||||
spyOn(rest, 'create').and.rejectWith(new Parse.Error(0, 'some other error'));
|
||||
Parse.Cloud.define('myFunction', () => {});
|
||||
const params = {
|
||||
|
||||
@@ -75,7 +75,7 @@ describe_only(() => {
|
||||
/**
|
||||
* Verifies simple passwords in GET login requests with special characters are scrubbed from the verbose log
|
||||
*/
|
||||
it_id('e36d6141-2a20-41d0-85fc-d1534c3e4bae')(it)('does scrub simple passwords on GET login', done => {
|
||||
it('does scrub simple passwords on GET login', done => {
|
||||
reconfigureServer({
|
||||
verbose: true,
|
||||
}).then(function () {
|
||||
@@ -105,7 +105,7 @@ describe_only(() => {
|
||||
/**
|
||||
* Verifies complex passwords in GET login requests with special characters are scrubbed from the verbose log
|
||||
*/
|
||||
it_id('24b277c5-250f-4a35-a449-2c8c519d4c03')(it)('does scrub complex passwords on GET login', done => {
|
||||
it('does scrub complex passwords on GET login', done => {
|
||||
reconfigureServer({
|
||||
verbose: true,
|
||||
})
|
||||
@@ -139,7 +139,7 @@ describe_only(() => {
|
||||
/**
|
||||
* Verifies fields in POST login requests are NOT present in the verbose log
|
||||
*/
|
||||
it_id('33143ec9-b32d-467c-ba65-ff2bbefdaadd')(it)('does not have password field in POST login', done => {
|
||||
it('does not have password field in POST login', done => {
|
||||
reconfigureServer({
|
||||
verbose: true,
|
||||
}).then(function () {
|
||||
|
||||
+48
-189
@@ -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,17 +17,14 @@ describe('middlewares', () => {
|
||||
},
|
||||
};
|
||||
fakeRes = jasmine.createSpyObj('fakeRes', ['end', 'status']);
|
||||
AppCachePut(fakeReq.body._ApplicationId, {});
|
||||
AppCache.put(fakeReq.body._ApplicationId, {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
AppCache.del(fakeReq.body._ApplicationId);
|
||||
});
|
||||
|
||||
it_id('4cc18d90-1763-4725-97fa-f63fb4692fc4')(it)('should use _ContentType if provided', done => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKeyIps: ['127.0.0.1'],
|
||||
});
|
||||
it('should use _ContentType if provided', done => {
|
||||
expect(fakeReq.headers['content-type']).toEqual(undefined);
|
||||
const contentType = 'image/jpeg';
|
||||
fakeReq.body._ContentType = contentType;
|
||||
@@ -46,37 +35,37 @@ describe('middlewares', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should give invalid response when keys are configured but no key supplied', async () => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
it('should give invalid response when keys are configured but no key supplied', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
restAPIKey: 'restAPIKey',
|
||||
});
|
||||
await middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
expect(fakeRes.status).toHaveBeenCalledWith(403);
|
||||
});
|
||||
|
||||
it('should give invalid response when keys are configured but supplied key is incorrect', async () => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
it('should give invalid response when keys are configured but supplied key is incorrect', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
restAPIKey: 'restAPIKey',
|
||||
});
|
||||
fakeReq.headers['x-parse-rest-api-key'] = 'wrongKey';
|
||||
await middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
expect(fakeRes.status).toHaveBeenCalledWith(403);
|
||||
});
|
||||
|
||||
it('should give invalid response when keys are configured but different key is supplied', async () => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
it('should give invalid response when keys are configured but different key is supplied', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
restAPIKey: 'restAPIKey',
|
||||
});
|
||||
fakeReq.headers['x-parse-client-key'] = 'clientKey';
|
||||
await middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
expect(fakeRes.status).toHaveBeenCalledWith(403);
|
||||
});
|
||||
|
||||
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_id('f9abd7ac-b1f4-4607-b9b0-365ff0559d84')(it)(`it should pull ${bodyKey} into req.info`, done => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKeyIps: ['0.0.0.0/0'],
|
||||
});
|
||||
fakeReq.ip = '127.0.0.1';
|
||||
|
||||
it(`it should pull ${bodyKey} into req.info`, done => {
|
||||
fakeReq.body[bodyKey] = keyValue;
|
||||
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||
expect(fakeReq.body[bodyKey]).toEqual(undefined);
|
||||
expect(fakeReq.info[infoKey]).toEqual(keyValue);
|
||||
@@ -147,64 +134,41 @@ describe('middlewares', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_id('4a0bce41-c536-4482-a873-12ed023380e2')(it)('should not succeed and log if the ip does not belong to masterKeyIps list', async () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
spyOn(logger, 'error').and.callFake(() => {});
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
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';
|
||||
|
||||
const error = await middlewares.handleParseHeaders(fakeReq, fakeRes, () => {}).catch(e => e);
|
||||
|
||||
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';
|
||||
|
||||
const error = await middlewares.handleParseHeaders(fakeReq, fakeRes, () => {}).catch(e => e);
|
||||
|
||||
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_id('2f7fadec-a87c-4626-90d1-65c75653aea9')(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_id('2b251fd4-d43c-48f4-ada9-c8458e40c12a')(it)('should allow any ip to use masterKey if masterKeyIps is empty', async () => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['0.0.0.0/0'],
|
||||
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 () => {
|
||||
@@ -228,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 = {};
|
||||
@@ -241,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, () => {});
|
||||
@@ -249,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 = {};
|
||||
@@ -265,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 = {};
|
||||
@@ -280,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 = {};
|
||||
@@ -294,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';
|
||||
@@ -335,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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,12 +18,6 @@ describe('MongoSchemaCollection', () => {
|
||||
},
|
||||
_metadata: {
|
||||
class_permissions: {
|
||||
ACL: {
|
||||
'*': {
|
||||
read: true,
|
||||
write: true,
|
||||
},
|
||||
},
|
||||
get: { '*': true },
|
||||
find: { '*': true },
|
||||
count: { '*': true },
|
||||
@@ -75,12 +69,6 @@ describe('MongoSchemaCollection', () => {
|
||||
objectId: { type: 'String' },
|
||||
},
|
||||
classLevelPermissions: {
|
||||
ACL: {
|
||||
'*': {
|
||||
read: true,
|
||||
write: true,
|
||||
},
|
||||
},
|
||||
find: { '*': true },
|
||||
get: { '*': true },
|
||||
count: { '*': true },
|
||||
|
||||
@@ -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 } },
|
||||
@@ -15,8 +16,8 @@ const fakeClient = {
|
||||
// These tests are specific to the mongo storage adapter + mongo storage format
|
||||
// and will eventually be moved into their own repo
|
||||
describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
beforeEach(async () => {
|
||||
await new MongoStorageAdapter({ uri: databaseURI }).deleteAllClasses();
|
||||
beforeEach(done => {
|
||||
new MongoStorageAdapter({ uri: databaseURI }).deleteAllClasses().then(done, fail);
|
||||
Config.get(Parse.applicationId).schemaCache.clear();
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -390,7 +308,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
await expectAsync(adapter.getClass('UnknownClass')).toBeRejectedWith(undefined);
|
||||
});
|
||||
|
||||
it_only_mongodb_version('<5.1 || >=6')('should use index for caseInsensitive query', async () => {
|
||||
it_only_mongodb_version('<5.1>=6')('should use index for caseInsensitive query', async () => {
|
||||
const user = new Parse.User();
|
||||
user.set('username', 'Bugs');
|
||||
user.set('password', 'Bunny');
|
||||
@@ -424,6 +342,40 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
expect(postIndexPlan.executionStats.executionStages.stage).toBe('FETCH');
|
||||
});
|
||||
|
||||
it_only_mongodb_version('>=5.1<6')('should use index for caseInsensitive query', async () => {
|
||||
const user = new Parse.User();
|
||||
user.set('username', 'Bugs');
|
||||
user.set('password', 'Bunny');
|
||||
await user.signUp();
|
||||
|
||||
const database = Config.get(Parse.applicationId).database;
|
||||
await database.adapter.dropAllIndexes('_User');
|
||||
|
||||
const preIndexPlan = await database.find(
|
||||
'_User',
|
||||
{ username: 'bugs' },
|
||||
{ caseInsensitive: true, explain: true }
|
||||
);
|
||||
|
||||
const schema = await new Parse.Schema('_User').get();
|
||||
|
||||
await database.adapter.ensureIndex(
|
||||
'_User',
|
||||
schema,
|
||||
['username'],
|
||||
'case_insensitive_username',
|
||||
true
|
||||
);
|
||||
|
||||
const postIndexPlan = await database.find(
|
||||
'_User',
|
||||
{ username: 'bugs' },
|
||||
{ caseInsensitive: true, explain: true }
|
||||
);
|
||||
expect(preIndexPlan.queryPlanner.winningPlan.queryPlan.stage).toBe('COLLSCAN');
|
||||
expect(postIndexPlan.queryPlanner.winningPlan.queryPlan.stage).toBe('FETCH');
|
||||
});
|
||||
|
||||
it('should delete field without index', async () => {
|
||||
const database = Config.get(Parse.applicationId).database;
|
||||
const obj = new Parse.Object('MyObject');
|
||||
@@ -449,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',
|
||||
@@ -649,418 +605,4 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('index creation options', () => {
|
||||
beforeEach(async () => {
|
||||
await new MongoStorageAdapter({ uri: databaseURI }).deleteAllClasses();
|
||||
});
|
||||
|
||||
async function getIndexes(collectionName) {
|
||||
const adapter = Config.get(Parse.applicationId).database.adapter;
|
||||
const collections = await adapter.database.listCollections({ name: collectionName }).toArray();
|
||||
if (collections.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return await adapter.database.collection(collectionName).indexes();
|
||||
}
|
||||
|
||||
it('should skip username index when createIndexUserUsername is false', async () => {
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI,
|
||||
databaseOptions: { createIndexUserUsername: false },
|
||||
});
|
||||
const indexes = await getIndexes('_User');
|
||||
expect(indexes.find(idx => idx.name === 'username_1')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should create username index when createIndexUserUsername is true', async () => {
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI,
|
||||
databaseOptions: { createIndexUserUsername: true },
|
||||
});
|
||||
const indexes = await getIndexes('_User');
|
||||
expect(indexes.find(idx => idx.name === 'username_1')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should skip case-insensitive username index when createIndexUserUsernameCaseInsensitive is false', async () => {
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI,
|
||||
databaseOptions: { createIndexUserUsernameCaseInsensitive: false },
|
||||
});
|
||||
const indexes = await getIndexes('_User');
|
||||
expect(indexes.find(idx => idx.name === 'case_insensitive_username')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should create case-insensitive username index when createIndexUserUsernameCaseInsensitive is true', async () => {
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI,
|
||||
databaseOptions: { createIndexUserUsernameCaseInsensitive: true },
|
||||
});
|
||||
const indexes = await getIndexes('_User');
|
||||
expect(indexes.find(idx => idx.name === 'case_insensitive_username')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should skip email index when createIndexUserEmail is false', async () => {
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI,
|
||||
databaseOptions: { createIndexUserEmail: false },
|
||||
});
|
||||
const indexes = await getIndexes('_User');
|
||||
expect(indexes.find(idx => idx.name === 'email_1')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should create email index when createIndexUserEmail is true', async () => {
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI,
|
||||
databaseOptions: { createIndexUserEmail: true },
|
||||
});
|
||||
const indexes = await getIndexes('_User');
|
||||
expect(indexes.find(idx => idx.name === 'email_1')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should skip case-insensitive email index when createIndexUserEmailCaseInsensitive is false', async () => {
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI,
|
||||
databaseOptions: { createIndexUserEmailCaseInsensitive: false },
|
||||
});
|
||||
const indexes = await getIndexes('_User');
|
||||
expect(indexes.find(idx => idx.name === 'case_insensitive_email')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should create case-insensitive email index when createIndexUserEmailCaseInsensitive is true', async () => {
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI,
|
||||
databaseOptions: { createIndexUserEmailCaseInsensitive: true },
|
||||
});
|
||||
const indexes = await getIndexes('_User');
|
||||
expect(indexes.find(idx => idx.name === 'case_insensitive_email')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should skip email verify token index when createIndexUserEmailVerifyToken is false', async () => {
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI,
|
||||
databaseOptions: { createIndexUserEmailVerifyToken: false },
|
||||
});
|
||||
const indexes = await getIndexes('_User');
|
||||
expect(indexes.find(idx => idx.name === '_email_verify_token' || idx.name === '_email_verify_token_1')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should create email verify token index when createIndexUserEmailVerifyToken is true', async () => {
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI,
|
||||
databaseOptions: { createIndexUserEmailVerifyToken: true },
|
||||
});
|
||||
const indexes = await getIndexes('_User');
|
||||
expect(indexes.find(idx => idx.name === '_email_verify_token' || idx.name === '_email_verify_token_1')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should skip password reset token index when createIndexUserPasswordResetToken is false', async () => {
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI,
|
||||
databaseOptions: { createIndexUserPasswordResetToken: false },
|
||||
});
|
||||
const indexes = await getIndexes('_User');
|
||||
expect(indexes.find(idx => idx.name === '_perishable_token' || idx.name === '_perishable_token_1')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should create password reset token index when createIndexUserPasswordResetToken is true', async () => {
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI,
|
||||
databaseOptions: { createIndexUserPasswordResetToken: true },
|
||||
});
|
||||
const indexes = await getIndexes('_User');
|
||||
expect(indexes.find(idx => idx.name === '_perishable_token' || idx.name === '_perishable_token_1')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should skip role name index when createIndexRoleName is false', async () => {
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI,
|
||||
databaseOptions: { createIndexRoleName: false },
|
||||
});
|
||||
const indexes = await getIndexes('_Role');
|
||||
expect(indexes.find(idx => idx.name === 'name_1')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should create role name index when createIndexRoleName is true', async () => {
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI,
|
||||
databaseOptions: { createIndexRoleName: true },
|
||||
});
|
||||
const indexes = await getIndexes('_Role');
|
||||
expect(indexes.find(idx => idx.name === 'name_1')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should create all indexes by default when options are undefined', async () => {
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI,
|
||||
databaseOptions: {},
|
||||
});
|
||||
|
||||
const userIndexes = await getIndexes('_User');
|
||||
const roleIndexes = await getIndexes('_Role');
|
||||
|
||||
// Verify all indexes are created with default behavior (backward compatibility)
|
||||
expect(userIndexes.find(idx => idx.name === 'username_1')).toBeDefined();
|
||||
expect(userIndexes.find(idx => idx.name === 'case_insensitive_username')).toBeDefined();
|
||||
expect(userIndexes.find(idx => idx.name === 'email_1')).toBeDefined();
|
||||
expect(userIndexes.find(idx => idx.name === 'case_insensitive_email')).toBeDefined();
|
||||
expect(userIndexes.find(idx => idx.name === '_email_verify_token' || idx.name === '_email_verify_token_1')).toBeDefined();
|
||||
expect(userIndexes.find(idx => idx.name === '_perishable_token' || idx.name === '_perishable_token_1')).toBeDefined();
|
||||
expect(roleIndexes.find(idx => idx.name === 'name_1')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('logClientEvents', () => {
|
||||
it('should log MongoDB client events when configured', async () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
const logSpy = spyOn(logger, 'warn');
|
||||
|
||||
const logClientEvents = [
|
||||
{
|
||||
name: 'serverDescriptionChanged',
|
||||
keys: ['address'],
|
||||
logLevel: 'warn',
|
||||
},
|
||||
];
|
||||
|
||||
const adapter = new MongoStorageAdapter({
|
||||
uri: databaseURI,
|
||||
mongoOptions: { logClientEvents },
|
||||
});
|
||||
|
||||
// Connect to trigger event listeners setup
|
||||
await adapter.connect();
|
||||
|
||||
// Manually trigger the event to test the listener
|
||||
const mockEvent = {
|
||||
address: 'localhost:27017',
|
||||
previousDescription: { type: 'Unknown' },
|
||||
newDescription: { type: 'Standalone' },
|
||||
};
|
||||
|
||||
adapter.client.emit('serverDescriptionChanged', mockEvent);
|
||||
|
||||
// Verify the log was called with the correct message
|
||||
expect(logSpy).toHaveBeenCalledWith(
|
||||
jasmine.stringMatching(/MongoDB client event serverDescriptionChanged:.*"address":"localhost:27017"/)
|
||||
);
|
||||
|
||||
await adapter.handleShutdown();
|
||||
});
|
||||
|
||||
it('should log entire event when keys are not specified', async () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
const logSpy = spyOn(logger, 'info');
|
||||
|
||||
const logClientEvents = [
|
||||
{
|
||||
name: 'connectionPoolReady',
|
||||
logLevel: 'info',
|
||||
},
|
||||
];
|
||||
|
||||
const adapter = new MongoStorageAdapter({
|
||||
uri: databaseURI,
|
||||
mongoOptions: { logClientEvents },
|
||||
});
|
||||
|
||||
await adapter.connect();
|
||||
|
||||
const mockEvent = {
|
||||
address: 'localhost:27017',
|
||||
options: { maxPoolSize: 100 },
|
||||
};
|
||||
|
||||
adapter.client.emit('connectionPoolReady', mockEvent);
|
||||
|
||||
expect(logSpy).toHaveBeenCalledWith(
|
||||
jasmine.stringMatching(/MongoDB client event connectionPoolReady:.*"address":"localhost:27017".*"options"/)
|
||||
);
|
||||
|
||||
await adapter.handleShutdown();
|
||||
});
|
||||
|
||||
it('should extract nested keys using dot notation', async () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
const logSpy = spyOn(logger, 'warn');
|
||||
|
||||
const logClientEvents = [
|
||||
{
|
||||
name: 'topologyDescriptionChanged',
|
||||
keys: ['previousDescription.type', 'newDescription.type', 'newDescription.servers.size'],
|
||||
logLevel: 'warn',
|
||||
},
|
||||
];
|
||||
|
||||
const adapter = new MongoStorageAdapter({
|
||||
uri: databaseURI,
|
||||
mongoOptions: { logClientEvents },
|
||||
});
|
||||
|
||||
await adapter.connect();
|
||||
|
||||
const mockEvent = {
|
||||
topologyId: 1,
|
||||
previousDescription: { type: 'Unknown' },
|
||||
newDescription: {
|
||||
type: 'ReplicaSetWithPrimary',
|
||||
servers: { size: 3 },
|
||||
},
|
||||
};
|
||||
|
||||
adapter.client.emit('topologyDescriptionChanged', mockEvent);
|
||||
|
||||
expect(logSpy).toHaveBeenCalledWith(
|
||||
jasmine.stringMatching(/MongoDB client event topologyDescriptionChanged:.*"previousDescription.type":"Unknown".*"newDescription.type":"ReplicaSetWithPrimary".*"newDescription.servers.size":3/)
|
||||
);
|
||||
|
||||
await adapter.handleShutdown();
|
||||
});
|
||||
|
||||
it('should handle invalid log level gracefully', async () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
const infoSpy = spyOn(logger, 'info');
|
||||
|
||||
const logClientEvents = [
|
||||
{
|
||||
name: 'connectionPoolReady',
|
||||
keys: ['address'],
|
||||
logLevel: 'invalidLogLevel', // Invalid log level
|
||||
},
|
||||
];
|
||||
|
||||
const adapter = new MongoStorageAdapter({
|
||||
uri: databaseURI,
|
||||
mongoOptions: { logClientEvents },
|
||||
});
|
||||
|
||||
await adapter.connect();
|
||||
|
||||
const mockEvent = {
|
||||
address: 'localhost:27017',
|
||||
};
|
||||
|
||||
adapter.client.emit('connectionPoolReady', mockEvent);
|
||||
|
||||
// Should fallback to 'info' level
|
||||
expect(infoSpy).toHaveBeenCalledWith(
|
||||
jasmine.stringMatching(/MongoDB client event connectionPoolReady:.*"address":"localhost:27017"/)
|
||||
);
|
||||
|
||||
await adapter.handleShutdown();
|
||||
});
|
||||
|
||||
it('should handle Map and Set instances in events', async () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
const warnSpy = spyOn(logger, 'warn');
|
||||
|
||||
const logClientEvents = [
|
||||
{
|
||||
name: 'customEvent',
|
||||
logLevel: 'warn',
|
||||
},
|
||||
];
|
||||
|
||||
const adapter = new MongoStorageAdapter({
|
||||
uri: databaseURI,
|
||||
mongoOptions: { logClientEvents },
|
||||
});
|
||||
|
||||
await adapter.connect();
|
||||
|
||||
const mockEvent = {
|
||||
mapData: new Map([['key1', 'value1'], ['key2', 'value2']]),
|
||||
setData: new Set([1, 2, 3]),
|
||||
};
|
||||
|
||||
adapter.client.emit('customEvent', mockEvent);
|
||||
|
||||
// Should serialize Map and Set properly
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
jasmine.stringMatching(/MongoDB client event customEvent:.*"mapData":\{"key1":"value1","key2":"value2"\}.*"setData":\[1,2,3\]/)
|
||||
);
|
||||
|
||||
await adapter.handleShutdown();
|
||||
});
|
||||
|
||||
it('should handle missing keys in event object', async () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
const infoSpy = spyOn(logger, 'info');
|
||||
|
||||
const logClientEvents = [
|
||||
{
|
||||
name: 'testEvent',
|
||||
keys: ['nonexistent.nested.key', 'another.missing'],
|
||||
logLevel: 'info',
|
||||
},
|
||||
];
|
||||
|
||||
const adapter = new MongoStorageAdapter({
|
||||
uri: databaseURI,
|
||||
mongoOptions: { logClientEvents },
|
||||
});
|
||||
|
||||
await adapter.connect();
|
||||
|
||||
const mockEvent = {
|
||||
actualField: 'value',
|
||||
};
|
||||
|
||||
adapter.client.emit('testEvent', mockEvent);
|
||||
|
||||
// Should handle missing keys gracefully with undefined values
|
||||
expect(infoSpy).toHaveBeenCalledWith(
|
||||
jasmine.stringMatching(/MongoDB client event testEvent:/)
|
||||
);
|
||||
|
||||
await adapter.handleShutdown();
|
||||
});
|
||||
|
||||
it('should handle circular references gracefully', async () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
const infoSpy = spyOn(logger, 'info');
|
||||
|
||||
const logClientEvents = [
|
||||
{
|
||||
name: 'circularEvent',
|
||||
logLevel: 'info',
|
||||
},
|
||||
];
|
||||
|
||||
const adapter = new MongoStorageAdapter({
|
||||
uri: databaseURI,
|
||||
mongoOptions: { logClientEvents },
|
||||
});
|
||||
|
||||
await adapter.connect();
|
||||
|
||||
// Create circular reference
|
||||
const mockEvent = { name: 'test' };
|
||||
mockEvent.self = mockEvent;
|
||||
|
||||
adapter.client.emit('circularEvent', mockEvent);
|
||||
|
||||
// Should handle circular reference with [Circular] marker
|
||||
expect(infoSpy).toHaveBeenCalledWith(
|
||||
jasmine.stringMatching(/MongoDB client event circularEvent:.*\[Circular\]/)
|
||||
);
|
||||
|
||||
await adapter.handleShutdown();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+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.
|
||||
|
||||
+43
-20
@@ -108,7 +108,7 @@ describe('Pages Router', () => {
|
||||
const res = await request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: `new_password=user1&token=43634643`,
|
||||
body: `new_password=user1&token=43634643&username=username`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
@@ -124,7 +124,7 @@ describe('Pages Router', () => {
|
||||
await request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: `new_password=&token=132414`,
|
||||
body: `new_password=&token=132414&username=Johnny`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
@@ -137,12 +137,30 @@ describe('Pages Router', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('request_password_reset: responds with AJAX error on missing username', async () => {
|
||||
try {
|
||||
await request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: `new_password=user1&token=43634643&username=`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
followRedirects: false,
|
||||
});
|
||||
} catch (error) {
|
||||
expect(error.status).not.toBe(302);
|
||||
expect(error.text).toEqual('{"code":200,"error":"Missing username"}');
|
||||
}
|
||||
});
|
||||
|
||||
it('request_password_reset: responds with AJAX error on missing token', async () => {
|
||||
try {
|
||||
await request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: `new_password=user1&token=`,
|
||||
body: `new_password=user1&token=&username=Johnny`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
@@ -559,7 +577,7 @@ describe('Pages Router', () => {
|
||||
spyOnProperty(Page.prototype, 'defaultFile').and.returnValue(jsonPageFile);
|
||||
|
||||
const response = await request({
|
||||
url: `http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&locale=${exampleLocale}`,
|
||||
url: `http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&username=exampleUsername&locale=${exampleLocale}`,
|
||||
followRedirects: false,
|
||||
}).catch(e => e);
|
||||
expect(response.status).toEqual(200);
|
||||
@@ -608,7 +626,7 @@ describe('Pages Router', () => {
|
||||
await reconfigureServer(config);
|
||||
const response = await request({
|
||||
url:
|
||||
'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&locale=de-AT',
|
||||
'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&username=exampleUsername&locale=de-AT',
|
||||
followRedirects: false,
|
||||
method: 'POST',
|
||||
});
|
||||
@@ -622,7 +640,7 @@ describe('Pages Router', () => {
|
||||
await reconfigureServer(config);
|
||||
const response = await request({
|
||||
url:
|
||||
'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&locale=de-AT',
|
||||
'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&username=exampleUsername&locale=de-AT',
|
||||
followRedirects: false,
|
||||
method: 'GET',
|
||||
});
|
||||
@@ -658,11 +676,13 @@ describe('Pages Router', () => {
|
||||
const appId = linkResponse.headers['x-parse-page-param-appid'];
|
||||
const token = linkResponse.headers['x-parse-page-param-token'];
|
||||
const locale = linkResponse.headers['x-parse-page-param-locale'];
|
||||
const username = linkResponse.headers['x-parse-page-param-username'];
|
||||
const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl'];
|
||||
const passwordResetPagePath = pageResponse.calls.all()[0].args[0];
|
||||
expect(appId).toBeDefined();
|
||||
expect(token).toBeDefined();
|
||||
expect(locale).toBeDefined();
|
||||
expect(username).toBeDefined();
|
||||
expect(publicServerUrl).toBeDefined();
|
||||
expect(passwordResetPagePath).toMatch(
|
||||
new RegExp(`\/${exampleLocale}\/${pages.passwordReset.defaultFile}`)
|
||||
@@ -676,6 +696,7 @@ describe('Pages Router', () => {
|
||||
body: {
|
||||
token,
|
||||
locale,
|
||||
username,
|
||||
new_password: 'newPassword',
|
||||
},
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
@@ -717,7 +738,7 @@ describe('Pages Router', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it_id('2845c2ea-23ba-45d2-a33f-63181d419bca')(it)('localizes end-to-end for verify email: success', async () => {
|
||||
it('localizes end-to-end for verify email: success', async () => {
|
||||
await reconfigureServer(config);
|
||||
const sendVerificationEmail = spyOn(
|
||||
config.emailAdapter,
|
||||
@@ -728,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);
|
||||
@@ -746,7 +766,7 @@ describe('Pages Router', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it_id('f2272b94-b4ac-474f-8e47-1ca74de136f5')(it)('localizes end-to-end for verify email: invalid verification link - link send success', async () => {
|
||||
it('localizes end-to-end for verify email: invalid verification link - link send success', async () => {
|
||||
await reconfigureServer(config);
|
||||
const sendVerificationEmail = spyOn(
|
||||
config.emailAdapter,
|
||||
@@ -757,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);
|
||||
@@ -772,13 +791,15 @@ describe('Pages Router', () => {
|
||||
|
||||
const appId = linkResponse.headers['x-parse-page-param-appid'];
|
||||
const locale = linkResponse.headers['x-parse-page-param-locale'];
|
||||
const username = linkResponse.headers['x-parse-page-param-username'];
|
||||
const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl'];
|
||||
const invalidVerificationPagePath = pageResponse.calls.all()[0].args[0];
|
||||
expect(appId).toBeDefined();
|
||||
expect(locale).toBe(exampleLocale);
|
||||
expect(username).toBeDefined();
|
||||
expect(publicServerUrl).toBeDefined();
|
||||
expect(invalidVerificationPagePath).toMatch(
|
||||
new RegExp(`\/${exampleLocale}\/${pages.emailVerificationLinkInvalid.defaultFile}`)
|
||||
new RegExp(`\/${exampleLocale}\/${pages.emailVerificationLinkExpired.defaultFile}`)
|
||||
);
|
||||
|
||||
const formUrl = `${publicServerUrl}/apps/${appId}/resend_verification_email`;
|
||||
@@ -787,7 +808,7 @@ describe('Pages Router', () => {
|
||||
method: 'POST',
|
||||
body: {
|
||||
locale,
|
||||
username: 'exampleUsername',
|
||||
username,
|
||||
},
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
followRedirects: false,
|
||||
@@ -798,7 +819,7 @@ describe('Pages Router', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it_id('1d46d36a-e455-4ae7-8717-e0d286e95f02')(it)('localizes end-to-end for verify email: invalid verification link - link send fail', async () => {
|
||||
it('localizes end-to-end for verify email: invalid verification link - link send fail', async () => {
|
||||
await reconfigureServer(config);
|
||||
const sendVerificationEmail = spyOn(
|
||||
config.emailAdapter,
|
||||
@@ -809,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);
|
||||
@@ -824,15 +844,15 @@ describe('Pages Router', () => {
|
||||
|
||||
const appId = linkResponse.headers['x-parse-page-param-appid'];
|
||||
const locale = linkResponse.headers['x-parse-page-param-locale'];
|
||||
const username = linkResponse.headers['x-parse-page-param-username'];
|
||||
const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl'];
|
||||
await jasmine.timeout();
|
||||
|
||||
const invalidVerificationPagePath = pageResponse.calls.all()[0].args[0];
|
||||
expect(appId).toBeDefined();
|
||||
expect(locale).toBe(exampleLocale);
|
||||
expect(username).toBeDefined();
|
||||
expect(publicServerUrl).toBeDefined();
|
||||
expect(invalidVerificationPagePath).toMatch(
|
||||
new RegExp(`\/${exampleLocale}\/${pages.emailVerificationLinkInvalid.defaultFile}`)
|
||||
new RegExp(`\/${exampleLocale}\/${pages.emailVerificationLinkExpired.defaultFile}`)
|
||||
);
|
||||
|
||||
spyOn(UserController.prototype, 'resendVerificationEmail').and.callFake(() =>
|
||||
@@ -845,7 +865,7 @@ describe('Pages Router', () => {
|
||||
method: 'POST',
|
||||
body: {
|
||||
locale,
|
||||
username: 'exampleUsername',
|
||||
username,
|
||||
},
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
followRedirects: false,
|
||||
@@ -1130,10 +1150,12 @@ describe('Pages Router', () => {
|
||||
|
||||
const appId = linkResponse.headers['x-parse-page-param-appid'];
|
||||
const token = linkResponse.headers['x-parse-page-param-token'];
|
||||
const username = linkResponse.headers['x-parse-page-param-username'];
|
||||
const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl'];
|
||||
const passwordResetPagePath = pageResponse.calls.all()[0].args[0];
|
||||
expect(appId).toBeDefined();
|
||||
expect(token).toBeDefined();
|
||||
expect(username).toBeDefined();
|
||||
expect(publicServerUrl).toBeDefined();
|
||||
expect(passwordResetPagePath).toMatch(new RegExp(`\/${pages.passwordReset.defaultFile}`));
|
||||
pageResponse.calls.reset();
|
||||
@@ -1144,6 +1166,7 @@ describe('Pages Router', () => {
|
||||
method: 'POST',
|
||||
body: {
|
||||
token,
|
||||
username,
|
||||
new_password: 'newPassword',
|
||||
},
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
@@ -1155,7 +1178,7 @@ describe('Pages Router', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it_id('81c1c28e-5dfd-4ffb-a09b-283156c08483')(it)('email verification works with custom endpoint', async () => {
|
||||
it('email verification works with custom endpoint', async () => {
|
||||
config.pages.pagesEndpoint = 'customEndpoint';
|
||||
await reconfigureServer(config);
|
||||
const sendVerificationEmail = spyOn(
|
||||
@@ -1167,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({
|
||||
@@ -1175,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 });
|
||||
}
|
||||
};
|
||||
@@ -111,7 +113,7 @@ const setup = function () {
|
||||
};
|
||||
|
||||
describe('Parse.Push', () => {
|
||||
it_id('d1e591c4-2b21-466b-9ee2-5be467b6b771')(it)('should properly send push', async () => {
|
||||
it('should properly send push', async () => {
|
||||
const { sendToInstallationSpy } = await setup();
|
||||
const pushStatusId = await Parse.Push.send({
|
||||
where: {
|
||||
@@ -126,7 +128,7 @@ describe('Parse.Push', () => {
|
||||
expect(sendToInstallationSpy.calls.count()).toEqual(10);
|
||||
});
|
||||
|
||||
it_id('2a58e3c7-b6f3-4261-a384-6c893b2ac3f3')(it)('should properly send push with lowercaseIncrement', async () => {
|
||||
it('should properly send push with lowercaseIncrement', async () => {
|
||||
await setup();
|
||||
const pushStatusId = await Parse.Push.send({
|
||||
where: {
|
||||
@@ -140,7 +142,7 @@ describe('Parse.Push', () => {
|
||||
await pushCompleted(pushStatusId);
|
||||
});
|
||||
|
||||
it_id('e21780b6-2cdd-467e-8013-81030f3288e9')(it)('should not allow clients to query _PushStatus', async () => {
|
||||
it('should not allow clients to query _PushStatus', async () => {
|
||||
await setup();
|
||||
const pushStatusId = await Parse.Push.send({
|
||||
where: {
|
||||
@@ -166,7 +168,7 @@ describe('Parse.Push', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it_id('924cf5f5-f684-4925-978a-e52c0c457366')(it)('should allow master key to query _PushStatus', async () => {
|
||||
it('should allow master key to query _PushStatus', async () => {
|
||||
await setup();
|
||||
const pushStatusId = await Parse.Push.send({
|
||||
where: {
|
||||
|
||||
+10
-30
@@ -5,9 +5,17 @@ const Config = require('../lib/Config');
|
||||
const auth = require('../lib/Auth');
|
||||
|
||||
describe('Parse.ACL', () => {
|
||||
it('acl must be valid', () => {
|
||||
it('acl must be valid', done => {
|
||||
const user = new Parse.User();
|
||||
expect(() => user.setACL('ACL')).toThrow(new Parse.Error(Parse.Error.OTHER_CAUSE, 'ACL must be a Parse ACL.'));
|
||||
ok(
|
||||
!user.setACL("Ceci n'est pas un ACL.", {
|
||||
error: function (user, error) {
|
||||
equal(error.code, -1);
|
||||
done();
|
||||
},
|
||||
}),
|
||||
'setACL should have returned false.'
|
||||
);
|
||||
});
|
||||
|
||||
it('refresh object with acl', async done => {
|
||||
@@ -923,32 +931,4 @@ describe('Parse.ACL', () => {
|
||||
|
||||
rest.create(config, auth.nobody(config), '_User', anonUser);
|
||||
});
|
||||
|
||||
it('support defaultACL in schema', async () => {
|
||||
await new Parse.Object('TestObject').save();
|
||||
const schema = await Parse.Server.database.loadSchema();
|
||||
await schema.updateClass(
|
||||
'TestObject',
|
||||
{},
|
||||
{
|
||||
create: {
|
||||
'*': true,
|
||||
},
|
||||
ACL: {
|
||||
'*': { read: true },
|
||||
currentUser: { read: true, write: true },
|
||||
},
|
||||
}
|
||||
);
|
||||
const acls = new Parse.ACL();
|
||||
acls.setPublicReadAccess(true);
|
||||
const user = await Parse.User.signUp('testuser', 'p@ssword');
|
||||
const obj = new Parse.Object('TestObject');
|
||||
await obj.save(null, { sessionToken: user.getSessionToken() });
|
||||
expect(obj.getACL()).toBeDefined();
|
||||
const acl = obj.getACL().toJSON();
|
||||
expect(acl['*']).toEqual({ read: true });
|
||||
expect(acl[user.id].write).toBeTrue();
|
||||
expect(acl[user.id].read).toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
+10
-28
@@ -33,7 +33,9 @@ describe('miscellaneous', () => {
|
||||
expect(results.length).toEqual(1);
|
||||
expect(results[0]['foo']).toEqual('bar');
|
||||
});
|
||||
});
|
||||
|
||||
describe('miscellaneous', function () {
|
||||
it('create a GameScore object', function (done) {
|
||||
const obj = new Parse.Object('GameScore');
|
||||
obj.set('score', 1337);
|
||||
@@ -161,7 +163,7 @@ describe('miscellaneous', () => {
|
||||
expect(numCreated).toBe(1);
|
||||
});
|
||||
|
||||
it_id('be1b9ac7-5e5f-4e91-b044-2bd8fb7622ad')(it)('ensure that if people already have duplicate users, they can still sign up new users', async done => {
|
||||
it('ensure that if people already have duplicate users, they can still sign up new users', async done => {
|
||||
try {
|
||||
await Parse.User.logOut();
|
||||
} catch (e) {
|
||||
@@ -207,7 +209,7 @@ describe('miscellaneous', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_id('d00f907e-41b9-40f6-8168-63e832199a8c')(it)('ensure that if people already have duplicate emails, they can still sign up new users', done => {
|
||||
it('ensure that if people already have duplicate emails, they can still sign up new users', done => {
|
||||
const config = Config.get('test');
|
||||
// Remove existing data to clear out unique index
|
||||
TestUtils.destroyAllDataPermanently()
|
||||
@@ -287,7 +289,7 @@ describe('miscellaneous', () => {
|
||||
}, fail);
|
||||
});
|
||||
|
||||
it_id('33db6efe-7c02-496c-8595-0ef627a94103')(it)('increment with a user object', function (done) {
|
||||
it('increment with a user object', function (done) {
|
||||
createTestUser()
|
||||
.then(user => {
|
||||
user.increment('foo');
|
||||
@@ -315,7 +317,7 @@ describe('miscellaneous', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it_id('bef99522-bcfd-4f79-ba9e-3c3845550401')(it)('save various data types', function (done) {
|
||||
it('save various data types', function (done) {
|
||||
const obj = new TestObject();
|
||||
obj.set('date', new Date());
|
||||
obj.set('array', [1, 2, 3]);
|
||||
@@ -949,7 +951,7 @@ describe('miscellaneous', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it_id('e9e718a9-4465-4158-b13e-f146855a8892')(it)('return the updated fields on PUT', async () => {
|
||||
it('return the updated fields on PUT', async () => {
|
||||
const obj = new Parse.Object('GameScore');
|
||||
const pointer = new Parse.Object('Child');
|
||||
await pointer.save();
|
||||
@@ -1021,7 +1023,7 @@ describe('miscellaneous', () => {
|
||||
expect(body.updatedAt).not.toBeUndefined();
|
||||
});
|
||||
|
||||
it_id('ea358b59-03c0-45c9-abc7-1fdd67573029')(it)('should response should not change with triggers', async () => {
|
||||
it('should response should not change with triggers', async () => {
|
||||
const obj = new Parse.Object('GameScore');
|
||||
const pointer = new Parse.Object('Child');
|
||||
Parse.Cloud.beforeSave('GameScore', request => {
|
||||
@@ -1265,26 +1267,6 @@ describe('miscellaneous', () => {
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
@@ -1489,7 +1471,7 @@ describe('miscellaneous', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_id('b2cd9cf2-13fa-4acd-aaa9-6f81fc1858db')(it)('properly returns incremented values (#1554)', done => {
|
||||
it('properly returns incremented values (#1554)', done => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
@@ -1678,7 +1660,7 @@ describe('miscellaneous', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_id('8f99ee20-3da7-45ec-b867-ea0eb87524a9')(it)('purge all objects in class', done => {
|
||||
it('purge all objects in class', done => {
|
||||
const object = new Parse.Object('TestObject');
|
||||
object.set('foo', 'bar');
|
||||
const object2 = new Parse.Object('TestObject');
|
||||
|
||||
@@ -1,144 +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({
|
||||
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({
|
||||
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({
|
||||
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({
|
||||
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_only_db('mongo')('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: {
|
||||
appName: 'MyParseApp',
|
||||
|
||||
// Cannot be tested as it requires authentication setup
|
||||
// authMechanism: 'SCRAM-SHA-256',
|
||||
// authMechanismProperties: { SERVICE_NAME: 'mongodb' },
|
||||
|
||||
authSource: 'admin',
|
||||
autoSelectFamily: true,
|
||||
autoSelectFamilyAttemptTimeout: 3000,
|
||||
compressors: ['zlib'],
|
||||
connectTimeoutMS: 5000,
|
||||
directConnection: false,
|
||||
disableIndexFieldValidation: true,
|
||||
forceServerObjectId: false,
|
||||
heartbeatFrequencyMS: 10000,
|
||||
localThresholdMS: 15,
|
||||
maxConnecting: 2,
|
||||
maxIdleTimeMS: 60000,
|
||||
maxPoolSize: 10,
|
||||
maxStalenessSeconds: 90,
|
||||
maxTimeMS: 1000,
|
||||
minPoolSize: 5,
|
||||
|
||||
// Cannot be tested as it requires a proxy setup
|
||||
// proxyHost: 'proxy.example.com',
|
||||
// proxyPassword: 'proxypass',
|
||||
// proxyPort: 1080,
|
||||
// proxyUsername: 'proxyuser',
|
||||
|
||||
readConcernLevel: 'majority',
|
||||
readPreference: 'secondaryPreferred',
|
||||
readPreferenceTags: [{ dc: 'east' }],
|
||||
|
||||
// Cannot be tested as it requires a replica set setup
|
||||
// replicaSet: 'myReplicaSet',
|
||||
|
||||
retryReads: true,
|
||||
retryWrites: true,
|
||||
serverMonitoringMode: 'auto',
|
||||
serverSelectionTimeoutMS: 5000,
|
||||
socketTimeoutMS: 5000,
|
||||
|
||||
// Cannot be tested as it requires a replica cluster setup
|
||||
// srvMaxHosts: 0,
|
||||
// srvServiceName: 'mongodb',
|
||||
|
||||
ssl: false,
|
||||
tls: false,
|
||||
tlsAllowInvalidCertificates: false,
|
||||
tlsAllowInvalidHostnames: false,
|
||||
tlsCAFile: __dirname + '/support/cert/cert.pem',
|
||||
tlsCertificateKeyFile: __dirname + '/support/cert/cert.pem',
|
||||
tlsCertificateKeyFilePassword: 'password',
|
||||
waitQueueTimeoutMS: 5000,
|
||||
zlibCompressionLevel: 6,
|
||||
},
|
||||
})).toBeResolved();
|
||||
await expectAsync(reconfigureServer({
|
||||
databaseURI: 'mongodb://localhost:27017/parse',
|
||||
filesAdapter: null,
|
||||
databaseAdapter: null,
|
||||
databaseOptions: {
|
||||
// The following option needs to be tested separately due to driver config rules
|
||||
tlsInsecure: false,
|
||||
},
|
||||
})).toBeResolved();
|
||||
expect(loggerErrorSpy.calls.all().reduce((s, call) => s += call.args[0], '')).not.toMatch(invalidKeyErrorMessage);
|
||||
});
|
||||
});
|
||||
+73
-387
@@ -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,48 +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('works without Content-Type and extension', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
},
|
||||
});
|
||||
const headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
const result = await request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/files/file',
|
||||
body: '<html></html>\n',
|
||||
});
|
||||
expect(result.data.url.includes('file.txt')).toBeTrue();
|
||||
expect(result.data.name.includes('file.txt')).toBeTrue();
|
||||
});
|
||||
|
||||
it('filename is url encoded', done => {
|
||||
@@ -653,80 +627,6 @@ describe('Parse.File testing', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('URI-backed file upload is disabled to prevent SSRF attack', () => {
|
||||
const express = require('express');
|
||||
let testServer;
|
||||
let testServerPort;
|
||||
let requestsMade;
|
||||
|
||||
beforeEach(async () => {
|
||||
requestsMade = [];
|
||||
const app = express();
|
||||
app.use((req, res) => {
|
||||
requestsMade.push({ url: req.url, method: req.method });
|
||||
res.status(200).send('test file content');
|
||||
});
|
||||
testServer = app.listen(0);
|
||||
testServerPort = testServer.address().port;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (testServer) {
|
||||
await new Promise(resolve => testServer.close(resolve));
|
||||
}
|
||||
Parse.Cloud._removeAllHooks();
|
||||
});
|
||||
|
||||
it('does not access URI when file upload attempted over REST', async () => {
|
||||
const response = await request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/classes/TestClass',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
body: {
|
||||
file: {
|
||||
__type: 'File',
|
||||
name: 'test.txt',
|
||||
_source: {
|
||||
format: 'uri',
|
||||
uri: `http://127.0.0.1:${testServerPort}/secret-file.txt`,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(response.status).toBe(201);
|
||||
// Verify no HTTP request was made to the URI
|
||||
expect(requestsMade.length).toBe(0);
|
||||
});
|
||||
|
||||
it('does not access URI when file created in beforeSave trigger', async () => {
|
||||
Parse.Cloud.beforeSave(Parse.File, () => {
|
||||
return new Parse.File('trigger-file.txt', {
|
||||
uri: `http://127.0.0.1:${testServerPort}/secret-file.txt`,
|
||||
});
|
||||
});
|
||||
await expectAsync(
|
||||
request({
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
url: 'http://localhost:8378/1/files/test.txt',
|
||||
body: 'test content',
|
||||
})
|
||||
).toBeRejectedWith(jasmine.objectContaining({
|
||||
status: 400
|
||||
}));
|
||||
// Verify no HTTP request was made to the URI
|
||||
expect(requestsMade.length).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleting files', () => {
|
||||
@@ -883,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',
|
||||
@@ -1350,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$/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+12
-12
@@ -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',
|
||||
@@ -93,7 +93,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it_id('bbd9e2f6-7f61-458f-98f2-4a563586cd8d')(it)('geo line', async done => {
|
||||
it('geo line', async done => {
|
||||
const line = [];
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
const obj = new TestObject();
|
||||
@@ -143,7 +143,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it_id('e1e86b38-b8a4-4109-8330-a324fe628e0c')(it)('geo max distance medium', async () => {
|
||||
it('geo max distance medium', async () => {
|
||||
const objects = [];
|
||||
[0, 1, 2].map(function (i) {
|
||||
const obj = new TestObject();
|
||||
@@ -207,7 +207,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it_id('05f1a454-56b1-4f2e-908e-408a9222cbae')(it)('geo max distance in km california', async () => {
|
||||
it('geo max distance in km california', async () => {
|
||||
await makeSomeGeoPoints();
|
||||
const sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
|
||||
const query = new Parse.Query(TestObject);
|
||||
@@ -246,7 +246,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
equal(results.length, 3);
|
||||
});
|
||||
|
||||
it_id('9ee376ad-dd6c-4c17-ad28-c7899a4411f1')(it)('geo max distance in miles california', async () => {
|
||||
it('geo max distance in miles california', async () => {
|
||||
await makeSomeGeoPoints();
|
||||
const sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
|
||||
const query = new Parse.Query(TestObject);
|
||||
@@ -276,7 +276,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
equal(results.length, 0);
|
||||
});
|
||||
|
||||
it_id('9e35a89e-bc2c-4ec5-b25a-8d1890a55233')(it)('returns nearest location', async () => {
|
||||
it('returns nearest location', async () => {
|
||||
await makeSomeGeoPoints();
|
||||
const sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
|
||||
const query = new Parse.Query(TestObject);
|
||||
@@ -286,7 +286,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
equal(results[1].get('name'), 'Sacramento');
|
||||
});
|
||||
|
||||
it_id('6df434b0-142d-4302-bbc6-a6ec5a9d9c68')(it)('works with geobox queries', done => {
|
||||
it('works with geobox queries', done => {
|
||||
const inbound = new Parse.GeoPoint(1.5, 1.5);
|
||||
const onbound = new Parse.GeoPoint(10, 10);
|
||||
const outbound = new Parse.GeoPoint(20, 20);
|
||||
@@ -356,7 +356,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_id('d9fbc5c6-f767-47d6-bb44-3858eb9df15a')(it)('supports withinPolygon open path', done => {
|
||||
it('supports withinPolygon open path', done => {
|
||||
const inbound = new Parse.GeoPoint(1.5, 1.5);
|
||||
const onbound = new Parse.GeoPoint(10, 10);
|
||||
const outbound = new Parse.GeoPoint(20, 20);
|
||||
@@ -394,7 +394,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
}, done.fail);
|
||||
});
|
||||
|
||||
it_id('3ec537bd-839a-4c93-a48b-b4a249820074')(it)('supports withinPolygon closed path', done => {
|
||||
it('supports withinPolygon closed path', done => {
|
||||
const inbound = new Parse.GeoPoint(1.5, 1.5);
|
||||
const onbound = new Parse.GeoPoint(10, 10);
|
||||
const outbound = new Parse.GeoPoint(20, 20);
|
||||
@@ -433,7 +433,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
}, done.fail);
|
||||
});
|
||||
|
||||
it_id('0a248e11-3598-480a-9ab5-8a0b259258e4')(it)('supports withinPolygon Polygon object', done => {
|
||||
it('supports withinPolygon Polygon object', done => {
|
||||
const inbound = new Parse.GeoPoint(1.5, 1.5);
|
||||
const onbound = new Parse.GeoPoint(10, 10);
|
||||
const outbound = new Parse.GeoPoint(20, 20);
|
||||
@@ -752,7 +752,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
equal(count, 1);
|
||||
});
|
||||
|
||||
it_id('0b073d31-0d41-41e7-bd60-f636ffb759dc')(it)('withinKilometers complex supports count', async () => {
|
||||
it('withinKilometers complex supports count', async () => {
|
||||
const inside = new Parse.GeoPoint(10, 10);
|
||||
const middle = new Parse.GeoPoint(20, 20);
|
||||
const outside = new Parse.GeoPoint(30, 30);
|
||||
@@ -770,7 +770,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
equal(count, 2);
|
||||
});
|
||||
|
||||
it_id('26c9a13d-3d71-452e-a91c-9a4589be021c')(it)('fails to fetch geopoints that are specifically not at (0,0)', async () => {
|
||||
it('fails to fetch geopoints that are specifically not at (0,0)', async () => {
|
||||
const tmp = new TestObject({
|
||||
location: new Parse.GeoPoint({ latitude: 0, longitude: 0 }),
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ const request = require('../lib/request');
|
||||
const Config = require('../lib/Config');
|
||||
|
||||
describe('a GlobalConfig', () => {
|
||||
beforeEach(async () => {
|
||||
beforeEach(done => {
|
||||
const config = Config.get('test');
|
||||
const query = on_db(
|
||||
'mongo',
|
||||
@@ -16,7 +16,7 @@ describe('a GlobalConfig', () => {
|
||||
return { objectId: '1' };
|
||||
}
|
||||
);
|
||||
await config.database.adapter
|
||||
config.database.adapter
|
||||
.upsertOneObject(
|
||||
'_GlobalConfig',
|
||||
{
|
||||
@@ -28,10 +28,14 @@ describe('a GlobalConfig', () => {
|
||||
},
|
||||
query,
|
||||
{
|
||||
params: { companies: ['US', 'DK'], counter: 20, internalParam: 'internal' },
|
||||
params: { companies: ['US', 'DK'], internalParam: 'internal' },
|
||||
masterKeyOnly: { internalParam: true },
|
||||
}
|
||||
);
|
||||
)
|
||||
.then(done, err => {
|
||||
jfail(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
const headers = {
|
||||
@@ -110,34 +114,6 @@ describe('a GlobalConfig', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_only_db('mongo')('can addUnique', async () => {
|
||||
await Parse.Config.save({ companies: { __op: 'AddUnique', objects: ['PA', 'RS', 'E'] } });
|
||||
const config = await Parse.Config.get();
|
||||
const companies = config.get('companies');
|
||||
expect(companies).toEqual(['US', 'DK', 'PA', 'RS', 'E']);
|
||||
});
|
||||
|
||||
it_only_db('mongo')('can add to array', async () => {
|
||||
await Parse.Config.save({ companies: { __op: 'Add', objects: ['PA'] } });
|
||||
const config = await Parse.Config.get();
|
||||
const companies = config.get('companies');
|
||||
expect(companies).toEqual(['US', 'DK', 'PA']);
|
||||
});
|
||||
|
||||
it_only_db('mongo')('can remove from array', async () => {
|
||||
await Parse.Config.save({ companies: { __op: 'Remove', objects: ['US'] } });
|
||||
const config = await Parse.Config.get();
|
||||
const companies = config.get('companies');
|
||||
expect(companies).toEqual(['DK']);
|
||||
});
|
||||
|
||||
it('can increment', async () => {
|
||||
await Parse.Config.save({ counter: { __op: 'Increment', amount: 49 } });
|
||||
const config = await Parse.Config.get();
|
||||
const counter = config.get('counter');
|
||||
expect(counter).toEqual(69);
|
||||
});
|
||||
|
||||
it('can add and retrive files', done => {
|
||||
request({
|
||||
method: 'PUT',
|
||||
@@ -181,7 +157,7 @@ describe('a GlobalConfig', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_id('5ebbd0cf-d1a5-49d9-aac7-5216abc5cb62')(it)('properly handles delete op', done => {
|
||||
it('properly handles delete op', done => {
|
||||
request({
|
||||
method: 'PUT',
|
||||
url: 'http://localhost:8378/1/config',
|
||||
@@ -189,7 +165,6 @@ describe('a GlobalConfig', () => {
|
||||
body: {
|
||||
params: {
|
||||
companies: { __op: 'Delete' },
|
||||
counter: { __op: 'Delete' },
|
||||
internalParam: { __op: 'Delete' },
|
||||
foo: 'bar',
|
||||
},
|
||||
@@ -208,7 +183,6 @@ describe('a GlobalConfig', () => {
|
||||
try {
|
||||
expect(response.status).toEqual(200);
|
||||
expect(body.params.companies).toBeUndefined();
|
||||
expect(body.params.counter).toBeUndefined();
|
||||
expect(body.params.foo).toBe('bar');
|
||||
expect(Object.keys(body.params).length).toBe(1);
|
||||
} catch (e) {
|
||||
|
||||
@@ -500,7 +500,7 @@ describe('ParseGraphQLSchema', () => {
|
||||
});
|
||||
});
|
||||
describe('alias', () => {
|
||||
it_id('45282d26-f4c7-4d2d-a7b6-cd8741d5322f')(it)('Should be able to define alias for get and find query', async () => {
|
||||
it('Should be able to define alias for get and find query', async () => {
|
||||
const parseGraphQLSchema = new ParseGraphQLSchema({
|
||||
databaseController,
|
||||
parseGraphQLController,
|
||||
@@ -535,7 +535,7 @@ describe('ParseGraphQLSchema', () => {
|
||||
expect(Object.keys(queries1)).toContain('precious_data');
|
||||
});
|
||||
|
||||
it_id('f04b46e3-a25d-401d-a315-3298cfee1df8')(it)('Should be able to define alias for mutation', async () => {
|
||||
it('Should be able to define alias for mutation', async () => {
|
||||
const parseGraphQLSchema = new ParseGraphQLSchema({
|
||||
databaseController,
|
||||
parseGraphQLController,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user