mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fc6e0a938 |
@@ -1,14 +1,9 @@
|
||||
{
|
||||
"plugins": [
|
||||
"@babel/plugin-transform-flow-strip-types",
|
||||
"@babel/plugin-proposal-object-rest-spread"
|
||||
"transform-flow-strip-types"
|
||||
],
|
||||
"presets": [
|
||||
["@babel/preset-env", {
|
||||
"targets": {
|
||||
"node": "8"
|
||||
}
|
||||
}]
|
||||
],
|
||||
"sourceMaps": "inline"
|
||||
"es2015",
|
||||
"stage-0"
|
||||
]
|
||||
}
|
||||
|
||||
+2
-3
@@ -1,3 +1,2 @@
|
||||
lib
|
||||
coverage
|
||||
out
|
||||
lib/*
|
||||
coverage/*
|
||||
|
||||
+2
-5
@@ -14,15 +14,12 @@
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"indent": ["error", 2, { "SwitchCase": 1 }],
|
||||
"indent": ["error", 2],
|
||||
"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"
|
||||
"prefer-const": "error"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,4 +7,3 @@
|
||||
[libs]
|
||||
|
||||
[options]
|
||||
suppress_comment= \\(.\\|\n\\)*\\@flow-disable-next
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
*.js text
|
||||
*.html text
|
||||
*.less text
|
||||
*.json text
|
||||
*.css text
|
||||
*.xml text
|
||||
*.md text
|
||||
*.txt text
|
||||
*.yml text
|
||||
*.sql text
|
||||
*.sh text
|
||||
|
||||
*.png binary
|
||||
@@ -0,0 +1,67 @@
|
||||
Please read the following instructions carefully.
|
||||
|
||||
Check out https://github.com/ParsePlatform/parse-server/issues/1271 for an ideal bug report.
|
||||
The closer your issue report is to that one, the more likely we are to be able to help, and the more likely we will be to fix the issue quickly!
|
||||
|
||||
Many members of the community use Stack Overflow and Server Fault to ask questions.
|
||||
Read through the existing questions or ask your own!
|
||||
- Stack Overflow: http://stackoverflow.com/questions/tagged/parse.com
|
||||
- Server Fault: https://serverfault.com/tags/parse
|
||||
|
||||
For database migration help, please file a bug report at https://parse.com/help#report
|
||||
|
||||
Make sure these boxes are checked before submitting your issue -- thanks for reporting issues back to Parse Server!
|
||||
|
||||
- [ ] You've met the prerequisites: https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#prerequisites.
|
||||
|
||||
- [ ] You're running the latest version of Parse Server: https://github.com/ParsePlatform/parse-server/releases
|
||||
|
||||
- [ ] You've searched through existing issues: https://github.com/ParsePlatform/Parse-Server/issues?utf8=%E2%9C%93&q=is%3Aissue Chances are that your issue has been reported or resolved before.
|
||||
|
||||
- [ ] You have filled out every section below. Issues without sufficient information are more likely to be closed.
|
||||
|
||||
--
|
||||
|
||||
### Issue Description
|
||||
|
||||
[DELETE EVERYTHING ABOVE THIS LINE BEFORE SUBMITTING YOUR ISSUE]
|
||||
|
||||
Describe your issue in as much detail as possible.
|
||||
|
||||
[FILL THIS OUT]
|
||||
|
||||
### Steps to reproduce
|
||||
|
||||
Please include a detailed list of steps that reproduce the issue. Include curl commands when applicable.
|
||||
|
||||
1. [FILL THIS OUT]
|
||||
2. [FILL THIS OUT]
|
||||
3. [FILL THIS OUT]
|
||||
|
||||
#### Expected Results
|
||||
|
||||
[FILL THIS OUT]
|
||||
|
||||
#### Actual Outcome
|
||||
|
||||
[FILL THIS OUT]
|
||||
|
||||
### Environment Setup
|
||||
|
||||
- **Server**
|
||||
- parse-server version (Be specific! Don't say 'latest'.) : [FILL THIS OUT]
|
||||
- Operating System: [FILL THIS OUT]
|
||||
- Hardware: [FILL THIS OUT]
|
||||
- Localhost or remote server? (AWS, Heroku, Azure, Digital Ocean, etc): [FILL THIS OUT]
|
||||
|
||||
- **Database**
|
||||
- MongoDB version: [FILL THIS OUT]
|
||||
- Storage engine: [FILL THIS OUT]
|
||||
- Hardware: [FILL THIS OUT]
|
||||
- Localhost or remote server? (AWS, mLab, ObjectRocket, Digital Ocean, etc): [FILL THIS OUT]
|
||||
|
||||
### Logs/Trace
|
||||
|
||||
You can turn on additional logging by configuring VERBOSE=1 in your environment.
|
||||
|
||||
[FILL THIS OUT]
|
||||
@@ -1,48 +0,0 @@
|
||||
---
|
||||
name: "\U0001F41B Report an issue"
|
||||
about: A feature of Parse Server is not working as expected.
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
### New Issue Checklist
|
||||
<!-- Please check the following boxes [ ] -> [x] before submitting your issue. Click the "Preview" tab for better readability. Thanks for reporting issues back to Parse Server! -->
|
||||
|
||||
- [ ] 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? -->
|
||||
|
||||
### Steps to reproduce
|
||||
<!-- How can someone else reproduce the issue? -->
|
||||
|
||||
### Actual Outcome
|
||||
<!-- What outcome, for example query result, did you get? -->
|
||||
|
||||
### 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". -->
|
||||
|
||||
Server
|
||||
- Parse Server version: `FILL_THIS_OUT`
|
||||
- Operating system: `FILL_THIS_OUT`
|
||||
- Local or remote host (AWS, Azure, Google Cloud, Heroku, Digital Ocean, etc): `FILL_THIS_OUT`
|
||||
|
||||
Database
|
||||
- System (MongoDB or Postgres): `FILL_THIS_OUT`
|
||||
- Database version: `FILL_THIS_OUT`
|
||||
- Local or remote host (MongoDB Atlas, mLab, AWS, Azure, Google Cloud, etc): `FILL_THIS_OUT`
|
||||
|
||||
Client
|
||||
- SDK (iOS, Android, JavaScript, PHP, Unity, etc): `FILL_THIS_OUT`
|
||||
- SDK version: `FILL_THIS_OUT`
|
||||
|
||||
### Logs
|
||||
<!-- Include relevant logs here. Turn on additional logging by configuring VERBOSE=1 in your environment. -->
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: "\U0001F4A1 Request a feature"
|
||||
about: Suggest new functionality or an enhancement of existing functionality.
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -1,8 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 🙋🏽♀️ Getting help with code
|
||||
url: https://stackoverflow.com/questions/tagged/parse-platform
|
||||
about: Get help with code-level questions on Stack Overflow.
|
||||
- name: 🙋 Getting general help
|
||||
url: https://community.parseplatform.org
|
||||
about: Get help with other questions on our Community Forum.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -1,19 +0,0 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 30
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 30
|
||||
# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
|
||||
onlyLabels:
|
||||
- ":rocket: feature"
|
||||
- ":dna: enhancement"
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: up-for-grabs
|
||||
# Limit to only `issues` not `pulls`
|
||||
only: issues
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as up-for-grabs because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
||||
@@ -1,115 +0,0 @@
|
||||
name: ci
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
env:
|
||||
COVERAGE_OPTION: ./node_modules/.bin/nyc
|
||||
NODE_VERSION: 10
|
||||
PARSE_SERVER_TEST_TIMEOUT: 20000
|
||||
jobs:
|
||||
check-mongo:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: Mongo 4.0.4, ReplicaSet, WiredTiger
|
||||
MONGODB_VERSION: 4.0.4
|
||||
MONGODB_TOPOLOGY: replicaset
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 10
|
||||
- name: Mongo 3.6.21
|
||||
MONGODB_VERSION: 3.6.21
|
||||
NODE_VERSION: 10
|
||||
- name: Redis Cache
|
||||
PARSE_SERVER_TEST_CACHE: redis
|
||||
NODE_VERSION: 10
|
||||
- name: Node 12.12.0
|
||||
NODE_VERSION: 12.12.0
|
||||
name: ${{ matrix.name }}
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-18.04
|
||||
services:
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
env:
|
||||
MONGODB_VERSION: ${{ matrix.MONGODB_VERSION }}
|
||||
MONGODB_TOPOLOGY: ${{ matrix.MONGODB_TOPOLOGY }}
|
||||
MONGODB_STORAGE_ENGINE: ${{ matrix.MONGODB_STORAGE_ENGINE }}
|
||||
PARSE_SERVER_TEST_CACHE: ${{ matrix.PARSE_SERVER_TEST_CACHE }}
|
||||
NODE_VERSION: ${{ matrix.NODE_VERSION }}
|
||||
steps:
|
||||
- name: Fix usage of insecure GitHub protocol
|
||||
run: sudo git config --system url."https://github".insteadOf "git://github"
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v1
|
||||
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
|
||||
- if: ${{ matrix.name == 'Mongo 3.6.21' }}
|
||||
run: npm run lint
|
||||
- run: npm run pretest
|
||||
- run: npm run coverage
|
||||
env:
|
||||
CI: true
|
||||
- run: bash <(curl -s https://codecov.io/bash)
|
||||
check-postgres:
|
||||
name: Postgresql
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-18.04
|
||||
services:
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
postgres:
|
||||
image: postgis/postgis:11-3.0
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
env:
|
||||
PARSE_SERVER_TEST_DB: postgres
|
||||
POSTGRES_MAJOR_VERSION: 11
|
||||
PARSE_SERVER_TEST_DATABASE_URI: postgres://postgres:postgres@localhost:5432/parse_server_postgres_adapter_test_database
|
||||
steps:
|
||||
- name: Fix usage of insecure GitHub protocol
|
||||
run: sudo git config --system url."https://github".insteadOf "git://github"
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 10
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 10
|
||||
- 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: bash scripts/before_script_postgres.sh
|
||||
- run: npm run coverage
|
||||
env:
|
||||
CI: true
|
||||
- run: bash <(curl -s https://codecov.io/bash)
|
||||
@@ -1,115 +0,0 @@
|
||||
name: release-automated
|
||||
on:
|
||||
push:
|
||||
branches: [ release, alpha, beta, next-major, release-4.x.x ]
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
current_tag: ${{ steps.tag.outputs.current_tag }}
|
||||
trigger_branch: ${{ steps.branch.outputs.trigger_branch }}
|
||||
steps:
|
||||
- name: Determine trigger branch name
|
||||
id: branch
|
||||
run: echo "::set-output name=trigger_branch::${GITHUB_REF#refs/*/}"
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
registry-url: https://registry.npmjs.org/
|
||||
- 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-
|
||||
- run: npm ci
|
||||
- run: npx semantic-release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
- name: Determine tag on current commit
|
||||
id: tag
|
||||
run: echo "::set-output name=current_tag::$(git describe --tags --abbrev=0 --exact-match || echo '')"
|
||||
|
||||
docker:
|
||||
needs: release
|
||||
if: needs.release.outputs.current_tag != ''
|
||||
env:
|
||||
REGISTRY: docker.io
|
||||
IMAGE_NAME: parseplatform/parse-server
|
||||
runs-on: ubuntu-18.04
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Determine branch name
|
||||
id: branch
|
||||
run: echo "::set-output name=branch_name::${GITHUB_REF#refs/*/}"
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ needs.release.outputs.current_tag }}
|
||||
- name: Set up QEMU
|
||||
id: qemu
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Log into Docker Hub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
flavor: |
|
||||
latest=${{ steps.branch.outputs.branch_name == 'release' }}
|
||||
tags: |
|
||||
type=semver,pattern={{version}},value=${{ needs.release.outputs.current_tag }}
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
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 }}
|
||||
|
||||
docs:
|
||||
needs: release
|
||||
if: needs.release.outputs.current_tag != '' && github.ref == 'refs/heads/release'
|
||||
runs-on: ubuntu-18.04
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
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: Generate Docs
|
||||
run: |
|
||||
echo $SOURCE_TAG
|
||||
npm ci
|
||||
./release_docs.sh
|
||||
env:
|
||||
SOURCE_TAG: ${{ needs.release.outputs.current_tag }}
|
||||
- name: Deploy
|
||||
uses: peaceiris/actions-gh-pages@v3.7.3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./docs
|
||||
@@ -1,57 +0,0 @@
|
||||
# Trigger this workflow only to manually create a Docker release; this should only be used
|
||||
# in extraordinary circumstances, as Docker releases are normally created automatically as
|
||||
# part of the automated release workflow.
|
||||
|
||||
name: release-manual-docker
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
default: ''
|
||||
description: 'Reference (tag / SHA):'
|
||||
env:
|
||||
REGISTRY: docker.io
|
||||
IMAGE_NAME: parseplatform/parse-server
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-18.04
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Determine branch name
|
||||
id: branch
|
||||
run: echo "::set-output name=branch_name::${GITHUB_REF#refs/*/}"
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.inputs.ref }}
|
||||
- name: Set up QEMU
|
||||
id: qemu
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Log into Docker Hub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
flavor: |
|
||||
latest=${{ steps.branch.outputs.branch_name == 'release' && github.event.inputs.ref == '' }}
|
||||
tags: |
|
||||
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@v2
|
||||
with:
|
||||
context: .
|
||||
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 }}
|
||||
@@ -13,11 +13,6 @@ lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
.nyc_output
|
||||
|
||||
# docs output
|
||||
out
|
||||
docs
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
@@ -56,6 +51,3 @@ lib/
|
||||
|
||||
# Folder created by FileSystemAdapter
|
||||
/files
|
||||
|
||||
# Redis Dump
|
||||
dump.rdb
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
instrumentation:
|
||||
excludes: ["**/spec/**"]
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"reporter": [
|
||||
"lcov",
|
||||
"text-summary"
|
||||
],
|
||||
"exclude": [
|
||||
"**/spec/**"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
semi: true
|
||||
trailingComma: "es5"
|
||||
singleQuote: true
|
||||
arrowParens: "avoid"
|
||||
printWidth: 100
|
||||
@@ -1,61 +0,0 @@
|
||||
*{{#if scope}} **{{scope}}:**
|
||||
{{~/if}} {{#if subject}}
|
||||
{{~subject}}
|
||||
{{~else}}
|
||||
{{~header}}
|
||||
{{~/if}}
|
||||
|
||||
{{~!-- commit link --}} {{#if @root.linkReferences~}}
|
||||
([{{shortHash}}](
|
||||
{{~#if @root.repository}}
|
||||
{{~#if @root.host}}
|
||||
{{~@root.host}}/
|
||||
{{~/if}}
|
||||
{{~#if @root.owner}}
|
||||
{{~@root.owner}}/
|
||||
{{~/if}}
|
||||
{{~@root.repository}}
|
||||
{{~else}}
|
||||
{{~@root.repoUrl}}
|
||||
{{~/if}}/
|
||||
{{~@root.commit}}/{{hash}}))
|
||||
{{~else}}
|
||||
{{~shortHash}}
|
||||
{{~/if}}
|
||||
|
||||
{{~!-- commit references --}}
|
||||
{{~#if references~}}
|
||||
, closes
|
||||
{{~#each references}} {{#if @root.linkReferences~}}
|
||||
[
|
||||
{{~#if this.owner}}
|
||||
{{~this.owner}}/
|
||||
{{~/if}}
|
||||
{{~this.repository}}#{{this.issue}}](
|
||||
{{~#if @root.repository}}
|
||||
{{~#if @root.host}}
|
||||
{{~@root.host}}/
|
||||
{{~/if}}
|
||||
{{~#if this.repository}}
|
||||
{{~#if this.owner}}
|
||||
{{~this.owner}}/
|
||||
{{~/if}}
|
||||
{{~this.repository}}
|
||||
{{~else}}
|
||||
{{~#if @root.owner}}
|
||||
{{~@root.owner}}/
|
||||
{{~/if}}
|
||||
{{~@root.repository}}
|
||||
{{~/if}}
|
||||
{{~else}}
|
||||
{{~@root.repoUrl}}
|
||||
{{~/if}}/
|
||||
{{~@root.issue}}/{{this.issue}})
|
||||
{{~else}}
|
||||
{{~#if this.owner}}
|
||||
{{~this.owner}}/
|
||||
{{~/if}}
|
||||
{{~this.repository}}#{{this.issue}}
|
||||
{{~/if}}{{/each}}
|
||||
{{~/if}}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
{{#if noteGroups}}
|
||||
{{#each noteGroups}}
|
||||
|
||||
### {{title}}
|
||||
|
||||
{{#each notes}}
|
||||
* {{#if commit.scope}}**{{commit.scope}}:** {{/if}}{{text}} ([{{commit.shortHash}}]({{commit.shortHash}}))
|
||||
{{/each}}
|
||||
{{/each}}
|
||||
|
||||
{{/if}}
|
||||
@@ -1,25 +0,0 @@
|
||||
{{#if isPatch~}}
|
||||
##
|
||||
{{~else~}}
|
||||
#
|
||||
{{~/if}} {{#if @root.linkCompare~}}
|
||||
[{{version}}](
|
||||
{{~#if @root.repository~}}
|
||||
{{~#if @root.host}}
|
||||
{{~@root.host}}/
|
||||
{{~/if}}
|
||||
{{~#if @root.owner}}
|
||||
{{~@root.owner}}/
|
||||
{{~/if}}
|
||||
{{~@root.repository}}
|
||||
{{~else}}
|
||||
{{~@root.repoUrl}}
|
||||
{{~/if~}}
|
||||
/compare/{{previousTag}}...{{currentTag}})
|
||||
{{~else}}
|
||||
{{~version}}
|
||||
{{~/if}}
|
||||
{{~#if title}} "{{title}}"
|
||||
{{~/if}}
|
||||
{{~#if date}} ({{date}})
|
||||
{{/if}}
|
||||
@@ -1,14 +0,0 @@
|
||||
{{> header}}
|
||||
|
||||
{{#each commitGroups}}
|
||||
|
||||
{{#if title}}
|
||||
### {{title}}
|
||||
|
||||
{{/if}}
|
||||
{{#each commits}}
|
||||
{{> commit root=@root}}
|
||||
{{/each}}
|
||||
{{/each}}
|
||||
|
||||
{{> footer}}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
language: node_js
|
||||
dist: trusty
|
||||
node_js:
|
||||
- '4.5'
|
||||
- '6.1'
|
||||
services:
|
||||
- postgresql
|
||||
- redis-server
|
||||
- docker
|
||||
addons:
|
||||
postgresql: '9.5'
|
||||
apt_packages:
|
||||
- postgresql-9.5-postgis-2.3
|
||||
before_script:
|
||||
- ls -al "$HOME/.mongodb/versions"
|
||||
- psql -c 'create database parse_server_postgres_adapter_test_database;' -U postgres
|
||||
- psql -c 'CREATE EXTENSION postgis;' -U postgres -d parse_server_postgres_adapter_test_database
|
||||
- psql -c 'CREATE EXTENSION postgis_topology;' -U postgres -d parse_server_postgres_adapter_test_database
|
||||
env:
|
||||
global:
|
||||
- COVERAGE_OPTION='./node_modules/.bin/istanbul cover'
|
||||
matrix:
|
||||
- MONGODB_VERSION=3.0.8
|
||||
- MONGODB_VERSION=3.2.6
|
||||
- PARSE_SERVER_TEST_DB=postgres
|
||||
- PARSE_SERVER_TEST_CACHE=redis
|
||||
matrix:
|
||||
fast_finish: true
|
||||
allow_failures:
|
||||
- env: PARSE_SERVER_TEST_DB=postgres
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- /^[0-9]+.[0-9]+.[0-9]+(-.*)?$/
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
- "$HOME/.mongodb/versions"
|
||||
after_script:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
after_failure:
|
||||
- (cd resources; python travis_after_all.py)
|
||||
after_success:
|
||||
- (cd resources; python travis_after_all.py)
|
||||
- export $(cat resources/.to_export_back)
|
||||
|
||||
deploy:
|
||||
- provider: npm
|
||||
skip_cleanup: true
|
||||
email:
|
||||
secure: T3LwSJFcu632SDfCoavcKL39NktWLEOOFzonAjeHAglmMyDp9hvU8xHwQ4JJy2CRA03c+ezAe2gH3Va+jfxvh1JtFUan+p88vaCHHKuNwPSp4WZBPt1ZTDmG+ACG6j+ZWHK+eP7hLvtlCi/M9/v4/bVojpm7v01LLiM+MRhnE9E7eLemQC4zb6RVtf2oGQ3mX51kMeya218kEm5bsQPpRJElI9jwletFa8qYK5AFgaCHpll059RRHQTTE9MTNcN58P/Kf6Qb3KGpKNoaHTXoOD4U6LcXLWvdHONVB6AzGefxg2b/dvtaO4nd5lDooMBR3u9kWpopXkyAuf+fO/8je9JTxO6CDUtDjHRcR2mCEXWX6rculIAbNXOv1ccRYCTQ8aFtYnFlMSb1+QIAdyT6HHmeT5ktk3+XJRuEv0PJJvqHTo+j7tPngjiv5sPNutgGFlOFO90omTbzEkIT/D/LsgXbWm2QKYWejbLiuSINg+FiFiZN90doCf4aCpm2y1Q/9GzvK+eXcpqzPCGxOykE7EuPZK3+S/ZU2oaWL2uSpbKPtC0qxebrneR307aVEa7C85HCkTMMBzE4tjUr8h5HhLqtWvqmDcnBo3PfQhf9cEO6jQLd3qhEVJmcwKz/yOCfgPXlCbRqiKk7j94perpNCIalXtMI9ySYjJNjHsB4600=
|
||||
api_key:
|
||||
secure: WLjhHVAdUkDy6UdNWveTpZqphw9olN0alCpyPpU0cJjlN/hk7YxPP+YHOSVnMZaRZoHM0LL11jPKLf970nymBfvJHDWxKk71c/5xyTX8pBtSxmGmGy23a5g2VrHVMd2JxsI7NEAH500tlFz+01t4E3Steo0NpAkHR3Q51kj01gQy8IumsksfhRc9uvjZ7zjm99Yk4L+cxyei79v4esfpx7Bgm0HTfAAAG/19F+r0hvvFiV517SREDU/YtcX/rIjVepEy1uiLKEohnmYtVIRSA3Hjh6VlHVhdj2WDd/dYrxi/Ioysq2zOM9pZVLamO9asB1e6JrTEipWE9jSZXxsbnfFzuNLxcOjKv1wd3OzQKF/7pGKGiWRTv5Xm19D/FrXoE/ULs6bHcC/Ke8Gs2RxPlOCdvAFehCkyf5P4HOPCQdR7o8Yiuvt+5JWKBflElNbSd4nWgwUOo3Yv8vC4Vj53fwmM+Uqfu3IgYFWktYxCm4RWIKMfB/gtmjcF6QYFfjvEMwAvRfBV81kuynCsnubXWzQeBE/b3JOhBfpGciKCjKfy+tS6bZfFjCtQV98hMMiCPre8Y7PahHDYc65wU9Ake+ZE+dDaSbeV3DZ5JeifLJHzKW2J2dWeRANkOaiSwO9VBC9/rht5ulK5qQ1pB2+sKDToAaiKy6RMlB/HgIoYqsU=
|
||||
on:
|
||||
tags: true
|
||||
all_branches: true
|
||||
condition: "$BUILD_LEADER$BUILD_AGGREGATE_STATUS = YESothers_succeeded"
|
||||
repo: ParsePlatform/parse-server
|
||||
- provider: script
|
||||
skip_cleanup: true
|
||||
script: ./resources/npm-git.sh
|
||||
on:
|
||||
branch: master
|
||||
condition: "$BUILD_LEADER$BUILD_AGGREGATE_STATUS = YESothers_succeeded"
|
||||
- provider: script
|
||||
skip_cleanup: true
|
||||
script: ./resources/docker-hub.sh
|
||||
on:
|
||||
tags: true
|
||||
all_branches: true
|
||||
condition: "$BUILD_LEADER$BUILD_AGGREGATE_STATUS = YESothers_succeeded"
|
||||
@@ -1,226 +0,0 @@
|
||||
# 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)
|
||||
|
||||
+5
-1285
File diff suppressed because it is too large
Load Diff
@@ -1,46 +0,0 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at codeofconduct@parseplatform.org. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
||||
+10
-104
@@ -1,110 +1,16 @@
|
||||
# Contributing to Parse Server
|
||||
### Contributing to Parse Server
|
||||
|
||||
We really want Parse to be yours, to see it grow and thrive in the open source community.
|
||||
#### Pull Requests Welcome!
|
||||
|
||||
If you are not familiar with Pull Requests and want to know more about them, you can visit the [Creating a pull request](https://help.github.com/articles/creating-a-pull-request/) article. It contains detailed informations about the process.
|
||||
We really want Parse to be yours, to see it grow and thrive in the open source community.
|
||||
|
||||
## Setting up the project for debugging and contributing:
|
||||
##### Please Do's
|
||||
|
||||
### Recommended setup:
|
||||
* Take testing seriously! Aim to increase the test coverage with every pull request.
|
||||
* Run the tests for the file you are working on with `npm test spec/MyFile.spec.js`
|
||||
* Run the tests for the whole project and look at the coverage report to make sure your tests are exhaustive by running `npm test` and looking at (project-root)/lcov-report/parse-server/FileUnderTest.js.html
|
||||
* Lint your code by running `npm run lint` to make sure all your code is not gonna be rejected by the CI.
|
||||
|
||||
* [vscode](https://code.visualstudio.com), the popular IDE.
|
||||
* [Jasmine Test Explorer](https://marketplace.visualstudio.com/items?itemName=hbenl.vscode-jasmine-test-adapter), a very practical test exploration plugin which let you run, debug and see the test results inline.
|
||||
##### Code of Conduct
|
||||
|
||||
### Setting up you local machine:
|
||||
|
||||
* [Fork](https://github.com/parse-community/parse-server) this project and clone the fork on your local machine:
|
||||
|
||||
```sh
|
||||
$ git clone https://github.com/parse-community/parse-server
|
||||
$ cd parse-server # go into the clone directory
|
||||
$ npm install # install all the node dependencies
|
||||
$ code . # launch vscode
|
||||
$ npm run watch # run babel watching for local file changes
|
||||
```
|
||||
|
||||
> To launch VS Code from the terminal with the `code` command you first need to follow the [launching from the command line section](https://code.visualstudio.com/docs/setup/mac#_launching-from-the-command-line) in the VS Code setup documentation.
|
||||
|
||||
Once you have babel running in watch mode, you can start making changes to parse-server.
|
||||
|
||||
### Good to know:
|
||||
|
||||
* The `lib/` folder is not commited, so never make changes in there.
|
||||
* Always make changes to files in the `src/` folder.
|
||||
* All the tests should point to sources in the `lib/` folder.
|
||||
|
||||
### Troubleshooting:
|
||||
|
||||
*Question*: I modify the code in the src folder but it doesn't seem to have any effect.<br/>
|
||||
*Answer*: Check that `npm run watch` is running
|
||||
|
||||
*Question*: How do I use breakpoints and debug step by step?<br/>
|
||||
*Answer*: The easiest way is to install [Jasmine Test Explorer](https://marketplace.visualstudio.com/items?itemName=hbenl.vscode-test-explorer), it will let you run selectively tests and debug them.
|
||||
|
||||
*Question*: How do I deploy my forked version on my servers?<br/>
|
||||
*Answer*: In your `package.json`, update the `parse-server` dependency to `https://github.com/MY_USERNAME/parse-server#MY_FEATURE`. Run `npm install`, commit the changes and deploy to your servers.
|
||||
|
||||
|
||||
### Please Do's
|
||||
|
||||
* Begin by reading the [Development Guide](http://docs.parseplatform.org/parse-server/guide/#development-guide) to learn how to get started running the parse-server.
|
||||
* Take testing seriously! Aim to increase the test coverage with every pull request. To obtain the test coverage of the project, run: `npm run coverage`
|
||||
* Run the tests for the file you are working on with the following command: `npm test spec/MyFile.spec.js`
|
||||
* Run the tests for the whole project to make sure the code passes all tests. This can be done by running the test command for a single file but removing the test file argument. The results can be seen at *<PROJECT_ROOT>/coverage/lcov-report/index.html*.
|
||||
* Lint your code by running `npm run lint` to make sure the code is not going to be rejected by the CI.
|
||||
* **Do not** publish the *lib* folder.
|
||||
|
||||
### Run your tests against Postgres (optional)
|
||||
|
||||
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.
|
||||
|
||||
* 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/.travis.yml#L43) 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:
|
||||
|
||||
- `describe_only_db('mongo')` // will create a `describe` that runs only on mongoDB
|
||||
- `it_only_db('mongo')` // will make a test that only runs on mongo
|
||||
- `it_exclude_dbs(['postgres'])` // will make a test that runs against all DB's but postgres
|
||||
* Similarly, if your feature is intended to only work with PostgreSQL, you should disable MongoDB-specific tests with:
|
||||
|
||||
- `describe_only_db('postgres')` // will create a `describe` that runs only on postgres
|
||||
- `it_only_db('postgres')` // will make a test that only runs on postgres
|
||||
- `it_exclude_dbs(['mongo'])` // will make a test that runs against all DB's but mongo
|
||||
|
||||
#### Run Postgres setup for Parse with Docker
|
||||
|
||||
[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:11-3.0-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 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:
|
||||
|
||||
```
|
||||
docker stop parse-postgres
|
||||
```
|
||||
|
||||
You can also use the [postgis/postgis:11-2.5-alpine](https://hub.docker.com/r/postgis/postgis) image in a Dockerfile and copy this [script](https://github.com/parse-community/parse-server/blob/master/scripts/before_script_postgres.sh) to the image by adding the following lines:
|
||||
|
||||
```
|
||||
#Install additional scripts. These are run in abc order during initial start
|
||||
COPY ./scripts/setup-dbs.sh /docker-entrypoint-initdb.d/setup-dbs.sh
|
||||
RUN chmod +x /docker-entrypoint-initdb.d/setup-dbs.sh
|
||||
```
|
||||
|
||||
Note that the script above will ONLY be executed during initialization of the container with no data in the database, see the official [Postgres image](https://hub.docker.com/_/postgres) for details. If you want to use the script to run again be sure there is no data in the /var/lib/postgresql/data of the container.
|
||||
|
||||
### Generate Parse Server Config Definition
|
||||
|
||||
If you want to make changes to [Parse Server Configuration][config] add the desired configuration to [src/Options/index.js][config-index] and run `npm run definitions`. This will output [src/Options/Definitions.js][config-def] and [src/Options/docs.js][config-docs].
|
||||
|
||||
To view docs run `npm run docs` and check the `/out` directory.
|
||||
|
||||
### Code of Conduct
|
||||
|
||||
This project adheres to the [Contributor Covenant Code of Conduct](https://github.com/parse-community/parse-server/blob/master/CODE_OF_CONDUCT.md). By participating, you are expected to honor this code.
|
||||
|
||||
[config]: http://parseplatform.org/parse-server/api/master/ParseServerOptions.html
|
||||
[config-def]: https://github.com/parse-community/parse-server/blob/master/src/Options/Definitions.js
|
||||
[config-docs]: https://github.com/parse-community/parse-server/blob/master/src/Options/docs.js
|
||||
[config-index]: https://github.com/parse-community/parse-server/blob/master/src/Options/index.js
|
||||
This project adheres to the [Open Code of Conduct](http://todogroup.org/opencodeofconduct/#Parse Server/fjm@fb.com). By participating, you are expected to honor this code.
|
||||
|
||||
+11
-43
@@ -1,53 +1,21 @@
|
||||
############################################################
|
||||
# Build stage
|
||||
############################################################
|
||||
FROM node:lts-alpine as build
|
||||
FROM node:boron
|
||||
|
||||
RUN apk update; \
|
||||
apk add git;
|
||||
WORKDIR /tmp
|
||||
RUN mkdir -p /parse-server
|
||||
COPY ./ /parse-server/
|
||||
|
||||
# Copy package.json first to benefit from layer caching
|
||||
COPY package*.json ./
|
||||
RUN mkdir -p /parse-server/config
|
||||
VOLUME /parse-server/config
|
||||
|
||||
# Copy src to have config files for install
|
||||
COPY . .
|
||||
|
||||
# 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:lts-alpine as release
|
||||
|
||||
RUN apk update; \
|
||||
apk add git;
|
||||
|
||||
VOLUME /parse-server/cloud /parse-server/config
|
||||
RUN mkdir -p /parse-server/cloud
|
||||
VOLUME /parse-server/cloud
|
||||
|
||||
WORKDIR /parse-server
|
||||
|
||||
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
|
||||
RUN npm install && \
|
||||
npm run build
|
||||
|
||||
ENV PORT=1337
|
||||
USER node
|
||||
|
||||
EXPOSE $PORT
|
||||
|
||||
ENTRYPOINT ["node", "./bin/parse-server"]
|
||||
ENTRYPOINT ["npm", "start", "--"]
|
||||
|
||||
@@ -28,7 +28,3 @@ 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.
|
||||
|
||||
-----
|
||||
|
||||
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.
|
||||
|
||||
@@ -31,7 +31,3 @@ 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,114 +1,36 @@
|
||||
<p align="center">
|
||||
<img alt="Parse Server" src="https://github.com/parse-community/parse-server/raw/master/.github/parse-server-logo.png" width="500">
|
||||
</a>
|
||||
</p>
|
||||

|
||||
|
||||
<p align="center">
|
||||
Parse Server is an open source backend that can be deployed to any infrastructure that can run Node.js.
|
||||
</p>
|
||||
[](https://travis-ci.org/ParsePlatform/parse-server)
|
||||
[](https://codecov.io/github/ParsePlatform/parse-server?branch=master)
|
||||
[](https://www.npmjs.com/package/parse-server)
|
||||
|
||||
[](https://gitter.im/ParsePlatform/Chat)
|
||||
|
||||
<p align="center">
|
||||
<a href="https://twitter.com/intent/follow?screen_name=parseplatform"><img alt="Follow on Twitter" src="https://img.shields.io/twitter/follow/parseplatform?style=social&label=Follow"></a>
|
||||
<a href="https://github.com/parse-community/parse-server/actions?query=workflow%3Aci+branch%3Amaster">
|
||||
<img alt="Build status" src="https://github.com/parse-community/parse-server/workflows/ci/badge.svg?branch=master">
|
||||
</a>
|
||||
<a href="https://codecov.io/github/parse-community/parse-server?branch=master"><img alt="Coverage status" src="https://img.shields.io/codecov/c/github/parse-community/parse-server/master.svg"></a>
|
||||
<a href="https://www.npmjs.com/package/parse-server"><img alt="npm version" src="https://img.shields.io/npm/v/parse-server.svg?style=flat"></a>
|
||||
<a href="https://community.parseplatform.org/"><img alt="Join the conversation" src="https://img.shields.io/discourse/https/community.parseplatform.org/topics.svg"></a>
|
||||
<a href="https://snyk.io/test/github/parse-community/parse-server"><img alt="Snyk badge" src="https://snyk.io/test/github/parse-community/parse-server/badge.svg"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img alt="MongoDB 3.6" src="https://img.shields.io/badge/mongodb-3.6-green.svg?logo=mongodb&style=flat">
|
||||
<img alt="MongoDB 4.0" src="https://img.shields.io/badge/mongodb-4.0-green.svg?logo=mongodb&style=flat">
|
||||
</p>
|
||||
|
||||
<h2 align="center">Our Sponsors</h2>
|
||||
<p align="center">
|
||||
<p align="center">Our backers and sponsors help to ensure the quality and timely development of the Parse Platform.</p>
|
||||
<details align="center">
|
||||
<summary align="center"><b>🥉 Bronze Sponsors</b></summary>
|
||||
<a href="https://opencollective.com/parse-server/sponsor/0/website" target="_blank"><img src="https://opencollective.com/parse-server/sponsor/0/avatar.svg"></a>
|
||||
</details>
|
||||
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="#backers"><img alt="Backers on Open Collective" src="https://opencollective.com/parse-server/tiers/backers/badge.svg" /></a>
|
||||
<a href="#sponsors"><img alt="Sponsors on Open Collective" src="https://opencollective.com/parse-server/tiers/sponsors/badge.svg" /></a>
|
||||
</p>
|
||||
<br>
|
||||
Parse Server is an [open source version of the Parse backend](http://blog.parse.com/announcements/introducing-parse-server-and-the-database-migration-tool/) that can be deployed to any infrastructure that can run Node.js.
|
||||
|
||||
Parse Server works with the Express web application framework. It can be added to existing web applications, or run by itself.
|
||||
|
||||
The full documentation for Parse Server is available in the [wiki](https://github.com/parse-community/parse-server/wiki). The [Parse Server guide](http://docs.parseplatform.org/parse-server/guide/) is a good place to get started. An [API reference](http://parseplatform.org/parse-server/api/) and [Cloud Code guide](https://docs.parseplatform.org/cloudcode/guide/) are also available. If you're interested in developing for Parse Server, the [Development guide](http://docs.parseplatform.org/parse-server/guide/#development-guide) will help you get set up.
|
||||
|
||||
- [Getting Started](#getting-started)
|
||||
- [Running Parse Server](#running-parse-server)
|
||||
- [Locally](#locally)
|
||||
- [Docker](#inside-a-docker-container)
|
||||
- [Saving an Object](#saving-your-first-object)
|
||||
- [Connect an SDK](#connect-your-app-to-parse-server)
|
||||
- [Running elsewhere](#running-parse-server-elsewhere)
|
||||
- [Sample Application](#parse-server-sample-application)
|
||||
- [Parse Server + Express](#parse-server--express)
|
||||
- [Configuration](#configuration)
|
||||
- [Basic Options](#basic-options)
|
||||
- [Client Key Options](#client-key-options)
|
||||
- [Email Verification & Password Reset](#email-verification-and-password-reset)
|
||||
- [Custom Pages](#custom-pages)
|
||||
- [Using Environment Variables](#using-environment-variables-to-configure-parse-server)
|
||||
- [Available Adapters](#available-adapters)
|
||||
- [Configuring File Adapters](#configuring-file-adapters)
|
||||
- [Logging](#logging)
|
||||
- [Live Queries](#live-queries)
|
||||
- [GraphQL](#graphql)
|
||||
- [Upgrading to 3.0.0](#upgrading-to-300)
|
||||
- [Support](#support)
|
||||
- [Ride the Bleeding Edge](#want-to-ride-the-bleeding-edge)
|
||||
- [Contributing](#contributing)
|
||||
- [Contributors](#contributors)
|
||||
- [Sponsors](#sponsors)
|
||||
- [Backers](#backers)
|
||||
|
||||
|
||||
# Getting Started
|
||||
|
||||
April 2016 - We created a series of video screencasts, please check them out here: [http://blog.parse.com/learn/parse-server-video-series-april-2016/](http://blog.parse.com/learn/parse-server-video-series-april-2016/)
|
||||
|
||||
The fastest and easiest way to get started is to run MongoDB and Parse Server locally.
|
||||
|
||||
## Running Parse Server
|
||||
|
||||
Before you start make sure you have installed:
|
||||
|
||||
- [NodeJS](https://www.npmjs.com/) that includes `npm`
|
||||
- [MongoDB](https://www.mongodb.com/) or [PostgreSQL](https://www.postgresql.org/)(with [PostGIS](https://postgis.net) 2.2.0 or higher)
|
||||
- Optionally [Docker](https://www.docker.com/)
|
||||
|
||||
### Locally
|
||||
```bash
|
||||
```
|
||||
$ npm install -g parse-server mongodb-runner
|
||||
$ mongodb-runner start
|
||||
$ parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --databaseURI mongodb://localhost/test
|
||||
```
|
||||
***Note:*** *If installation with* `-g` *fails due to permission problems* (`npm ERR! code 'EACCES'`), *please refer to [this link](https://docs.npmjs.com/getting-started/fixing-npm-permissions).*
|
||||
|
||||
|
||||
### Inside a Docker container
|
||||
|
||||
```bash
|
||||
$ git clone https://github.com/parse-community/parse-server
|
||||
$ cd parse-server
|
||||
```
|
||||
$ docker build --tag parse-server .
|
||||
$ docker run --name my-mongo -d mongo
|
||||
```
|
||||
|
||||
#### Running the Parse Server Image
|
||||
|
||||
```bash
|
||||
$ docker run --name my-parse-server -v config-vol:/parse-server/config -p 1337:1337 --link my-mongo:mongo -d parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --databaseURI mongodb://mongo/test
|
||||
```
|
||||
|
||||
***Note:*** *If you want to use [Cloud Code](https://docs.parseplatform.org/cloudcode/guide/) feature, please add `-v cloud-code-vol:/parse-server/cloud --cloud /parse-server/cloud/main.js` to command above. Make sure the `main.js` file is available in the `cloud-code-vol` directory before run this command. Otherwise, an error will occur.*
|
||||
$ docker run --name my-parse-server --link my-mongo:mongo parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --databaseURI mongodb://mongo/test
|
||||
```
|
||||
|
||||
You can use any arbitrary string as your application id and master key. These will be used by your clients to authenticate with the Parse Server.
|
||||
|
||||
@@ -118,10 +40,10 @@ That's it! You are now running a standalone version of Parse Server on your mach
|
||||
|
||||
### Saving your first object
|
||||
|
||||
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:
|
||||
Now that you're running Parse Server, it is time to save your first object. We'll use the [REST API](https://parse.com/docs/rest/guide), but you can easily do the same using any of the [Parse SDKs](https://parseplatform.github.io/#sdks). Run the following:
|
||||
|
||||
```bash
|
||||
$ curl -X POST \
|
||||
curl -X POST \
|
||||
-H "X-Parse-Application-Id: APPLICATION_ID" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"score":1337,"playerName":"Sean Plott","cheatMode":false}' \
|
||||
@@ -158,7 +80,7 @@ $ curl -X GET \
|
||||
|
||||
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
|
||||
@@ -180,19 +102,19 @@ $ curl -X GET \
|
||||
|
||||
```
|
||||
|
||||
To learn more about using saving and querying objects on Parse Server, check out the [Parse documentation](http://docs.parseplatform.org).
|
||||
To learn more about using saving and querying objects on Parse Server, check out the [Parse documentation](https://parse.com/docs).
|
||||
|
||||
### Connect your app to Parse Server
|
||||
|
||||
Parse provides SDKs for all the major platforms. Refer to the Parse Server guide to [learn how to connect your app to Parse Server](https://docs.parseplatform.org/parse-server/guide/#using-parse-sdks-with-parse-server).
|
||||
Parse provides SDKs for all the major platforms. Refer to the Parse Server guide to [learn how to connect your app to Parse Server](https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#using-parse-sdks-with-parse-server).
|
||||
|
||||
## Running Parse Server elsewhere
|
||||
|
||||
Once you have a better understanding of how the project works, please refer to the [Parse Server wiki](https://github.com/parse-community/parse-server/wiki) for in-depth guides to deploy Parse Server to major infrastructure providers. Read on to learn more about additional ways of running Parse Server.
|
||||
Once you have a better understanding of how the project works, please refer to the [Parse Server wiki](https://github.com/ParsePlatform/parse-server/wiki) for in-depth guides to deploy Parse Server to major infrastructure providers. Read on to learn more about additional ways of running Parse Server.
|
||||
|
||||
### Parse Server Sample Application
|
||||
|
||||
We have provided a basic [Node.js application](https://github.com/parse-community/parse-server-example) that uses the Parse Server module on Express and can be easily deployed to various infrastructure providers:
|
||||
We have provided a basic [Node.js application](https://github.com/ParsePlatform/parse-server-example) that uses the Parse Server module on Express and can be easily deployed to various infrastructure providers:
|
||||
|
||||
* [Heroku and mLab](https://devcenter.heroku.com/articles/deploying-a-parse-server-to-heroku)
|
||||
* [AWS and Elastic Beanstalk](http://mobile.awsblog.com/post/TxCD57GZLM2JR/How-to-set-up-Parse-Server-on-AWS-using-AWS-Elastic-Beanstalk)
|
||||
@@ -202,8 +124,7 @@ We have provided a basic [Node.js application](https://github.com/parse-communit
|
||||
* [Digital Ocean](https://www.digitalocean.com/community/tutorials/how-to-run-parse-server-on-ubuntu-14-04)
|
||||
* [Pivotal Web Services](https://github.com/cf-platform-eng/pws-parse-server)
|
||||
* [Back4app](http://blog.back4app.com/2016/03/01/quick-wizard-migration/)
|
||||
* [Glitch](https://glitch.com/edit/#!/parse-server)
|
||||
* [Flynn](https://flynn.io/blog/parse-apps-on-flynn)
|
||||
* [Gomix](https://gomix.com/#!/project/parse-server)
|
||||
|
||||
### Parse Server + Express
|
||||
|
||||
@@ -216,7 +137,7 @@ var app = express();
|
||||
|
||||
var api = new ParseServer({
|
||||
databaseURI: 'mongodb://localhost:27017/dev', // Connection string for your MongoDB database
|
||||
cloud: './cloud/main.js', // Path to your Cloud Code
|
||||
cloud: '/home/myApp/cloud/main.js', // Absolute path to your Cloud Code
|
||||
appId: 'myAppId',
|
||||
masterKey: 'myMasterKey', // Keep this key secret!
|
||||
fileKey: 'optionalFileKey',
|
||||
@@ -231,15 +152,37 @@ app.listen(1337, function() {
|
||||
});
|
||||
```
|
||||
|
||||
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).
|
||||
For a full list of available options, run `parse-server --help`.
|
||||
|
||||
## Logging
|
||||
|
||||
Parse Server will, by default, will log:
|
||||
* to the console
|
||||
* daily rotating files as new line delimited JSON
|
||||
|
||||
Logs are also be viewable in Parse Dashboard.
|
||||
|
||||
**Want to log each request and response?** Set the `VERBOSE` environment variable when starting `parse-server`. Usage :- `VERBOSE='1' parse-server --appId APPLICATION_ID --masterKey MASTER_KEY`
|
||||
|
||||
**Want logs to be in placed in other folder?** Pass the `PARSE_SERVER_LOGS_FOLDER` environment variable when starting `parse-server`. Usage :- `PARSE_SERVER_LOGS_FOLDER='<path-to-logs-folder>' parse-server --appId APPLICATION_ID --masterKey MASTER_KEY`
|
||||
|
||||
**Want new line delimited JSON error logs (for consumption by CloudWatch, Google Cloud Logging, etc.)?** Pass the `JSON_LOGS` environment variable when starting `parse-server`. Usage :- `JSON_LOGS='1' parse-server --appId APPLICATION_ID --masterKey MASTER_KEY`
|
||||
|
||||
# Documentation
|
||||
|
||||
The full documentation for Parse Server is available in the [wiki](https://github.com/ParsePlatform/parse-server/wiki). The [Parse Server guide](https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide) is a good place to get started. If you're interested in developing for Parse Server, the [Development guide](https://github.com/ParsePlatform/parse-server/wiki/Development-Guide) will help you get set up.
|
||||
|
||||
## Migrating an Existing Parse App
|
||||
|
||||
The hosted version of Parse will be fully retired on January 28th, 2017. If you are planning to migrate an app, you need to begin work as soon as possible. There are a few areas where Parse Server does not provide compatibility with the hosted version of Parse. Learn more in the [Migration guide](https://parse.com/migration).
|
||||
|
||||
## 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](http://parseplatform.org/parse-server/api/master/ParseServerOptions.html).
|
||||
For the full list of available options, run `parse-server --help`.
|
||||
|
||||
### Basic options
|
||||
#### Basic options
|
||||
|
||||
* `appId` **(required)** - The application id to host with this server instance. You can use any arbitrary string. For migrated apps, this should match your hosted Parse app.
|
||||
* `masterKey` **(required)** - The master key to use for overriding ACL security. You can use any arbitrary string. Keep it secret! For migrated apps, this should match your hosted Parse app.
|
||||
@@ -247,9 +190,9 @@ For the full list of available options, run `parse-server --help` or take a look
|
||||
* `port` - The default port is 1337, specify this parameter to use a different port.
|
||||
* `serverURL` - URL to your Parse Server (don't forget to specify http:// or https://). This URL will be used when making requests to Parse Server from Cloud Code.
|
||||
* `cloud` - The absolute path to your cloud code `main.js` file.
|
||||
* `push` - Configuration options for APNS and GCM push. See the [Push Notifications quick start](http://docs.parseplatform.org/parse-server/guide/#push-notifications_push-notifications-quick-start).
|
||||
* `push` - Configuration options for APNS and GCM push. See the [Push Notifications wiki entry](https://github.com/ParsePlatform/parse-server/wiki/Push).
|
||||
|
||||
### Client key options
|
||||
#### Client key options
|
||||
|
||||
The client keys used with Parse are no longer necessary with Parse Server. If you wish to still require them, perhaps to be able to refuse access to older clients, you can set the keys at initialization time. Setting any of these keys will require all requests to provide one of the configured keys.
|
||||
|
||||
@@ -258,7 +201,33 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo
|
||||
* `restAPIKey`
|
||||
* `dotNetKey`
|
||||
|
||||
### Email verification and password reset
|
||||
#### Advanced options
|
||||
|
||||
* `fileKey` - For migrated apps, this is necessary to provide access to files already hosted on Parse.
|
||||
* `allowClientClassCreation` - Set to false to disable client class creation. Defaults to true.
|
||||
* `enableAnonymousUsers` - Set to false to disable anonymous users. Defaults to true.
|
||||
* `oauth` - Used to configure support for [3rd party authentication](https://github.com/ParsePlatform/parse-server/wiki/OAuth).
|
||||
* `facebookAppIds` - An array of valid Facebook application IDs that users may authenticate with.
|
||||
* `mountPath` - Mount path for the server. Defaults to `/parse`.
|
||||
* `filesAdapter` - The default behavior (GridStore) can be changed by creating an adapter class (see [`FilesAdapter.js`](https://github.com/ParsePlatform/parse-server/blob/master/src/Adapters/Files/FilesAdapter.js)).
|
||||
* `maxUploadSize` - Max file size for uploads. Defaults to 20 MB.
|
||||
* `loggerAdapter` - The default behavior/transport (File) can be changed by creating an adapter class (see [`LoggerAdapter.js`](https://github.com/ParsePlatform/parse-server/blob/master/src/Adapters/Logger/LoggerAdapter.js)).
|
||||
* `sessionLength` - The length of time in seconds that a session should be valid for. Defaults to 31536000 seconds (1 year).
|
||||
* `revokeSessionOnPasswordReset` - When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions.
|
||||
* `accountLockout` - Lock account when a malicious user is attempting to determine an account password by trial and error.
|
||||
* `passwordPolicy` - Optional password policy rules to enforce.
|
||||
|
||||
##### Logging
|
||||
|
||||
Use the `PARSE_SERVER_LOGS_FOLDER` environment variable when starting `parse-server` to save your server logfiles to the specified folder.
|
||||
|
||||
Usage:
|
||||
|
||||
```
|
||||
PARSE_SERVER_LOGS_FOLDER='<path-to-logs-folder>' parse-server --appId APPLICATION_ID --masterKey MASTER_KEY
|
||||
```
|
||||
|
||||
##### Email verification and password reset
|
||||
|
||||
Verifying user email addresses and enabling password reset via email requires an email adapter. As part of the `parse-server` package we provide an adapter for sending email through Mailgun. To use it, sign up for Mailgun, and add this to your initialization code:
|
||||
|
||||
@@ -291,7 +260,7 @@ var server = ParseServer({
|
||||
appName: 'Parse App',
|
||||
// The email adapter
|
||||
emailAdapter: {
|
||||
module: '@parse/simple-mailgun-adapter',
|
||||
module: 'parse-server-simple-mailgun-adapter',
|
||||
options: {
|
||||
// The address that your emails come from
|
||||
fromAddress: 'parse@example.com',
|
||||
@@ -310,15 +279,14 @@ var server = ParseServer({
|
||||
},
|
||||
// optional settings to enforce password policies
|
||||
passwordPolicy: {
|
||||
// Two optional settings to enforce strong passwords. Either one or both can be specified.
|
||||
// Two optional settings to enforce strong passwords. Either one or both can be specified.
|
||||
// If both are specified, both checks must pass to accept the password
|
||||
// 1. a RegExp object or a regex string representing the pattern to enforce
|
||||
// 1. a RegExp representing the pattern to enforce
|
||||
validatorPattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{8,})/, // enforce password with at least 8 char with at least 1 lower case, 1 upper case and 1 digit
|
||||
// 2. a callback function to be invoked to validate the password
|
||||
validatorCallback: (password) => { return validatePassword(password) },
|
||||
validationError: 'Password must contain at least 1 digit.' // optional error message to be sent instead of the default "Password does not meet the Password Policy requirements." message.
|
||||
// 2. a callback function to be invoked to validate the password
|
||||
validatorCallback: (password) => { return validatePassword(password) },
|
||||
doNotAllowUsername: true, // optional setting to disallow username in passwords
|
||||
maxPasswordAge: 90, // optional setting in days for password expiry. Login fails if user does not reset the password within this period after signup/last reset.
|
||||
maxPasswordAge: 90, // optional setting in days for password expiry. Login fails if user does not reset the password within this period after signup/last reset.
|
||||
maxPasswordHistory: 5, // optional setting to prevent reuse of previous n passwords. Maximum value that can be specified is 20. Not specifying it or specifying 0 will not enforce history.
|
||||
//optional setting to set a validity duration for password reset links (in seconds)
|
||||
resetTokenValidityDuration: 24*60*60, // expire after 24 hours
|
||||
@@ -327,37 +295,13 @@ var server = ParseServer({
|
||||
```
|
||||
|
||||
You can also use other email adapters contributed by the community such as:
|
||||
- [parse-smtp-template (Multi Language and Multi Template)](https://www.npmjs.com/package/parse-smtp-template)
|
||||
- [parse-server-postmark-adapter](https://www.npmjs.com/package/parse-server-postmark-adapter)
|
||||
- [parse-server-sendgrid-adapter](https://www.npmjs.com/package/parse-server-sendgrid-adapter)
|
||||
- [parse-server-mandrill-adapter](https://www.npmjs.com/package/parse-server-mandrill-adapter)
|
||||
- [parse-server-simple-ses-adapter](https://www.npmjs.com/package/parse-server-simple-ses-adapter)
|
||||
- [parse-server-mailgun-adapter-template](https://www.npmjs.com/package/parse-server-mailgun-adapter-template)
|
||||
- [parse-server-sendinblue-adapter](https://www.npmjs.com/package/parse-server-sendinblue-adapter)
|
||||
- [parse-server-mailjet-adapter](https://www.npmjs.com/package/parse-server-mailjet-adapter)
|
||||
- [simple-parse-smtp-adapter](https://www.npmjs.com/package/simple-parse-smtp-adapter)
|
||||
- [parse-server-generic-email-adapter](https://www.npmjs.com/package/parse-server-generic-email-adapter)
|
||||
|
||||
### Custom Pages
|
||||
|
||||
It’s possible to change the default pages of the app and redirect the user to another path or domain.
|
||||
|
||||
```js
|
||||
var server = ParseServer({
|
||||
...otherOptions,
|
||||
|
||||
customPages: {
|
||||
passwordResetSuccess: "http://yourapp.com/passwordResetSuccess",
|
||||
verifyEmailSuccess: "http://yourapp.com/verifyEmailSuccess",
|
||||
parseFrameURL: "http://yourapp.com/parseFrameURL",
|
||||
linkSendSuccess: "http://yourapp.com/linkSendSuccess",
|
||||
linkSendFail: "http://yourapp.com/linkSendFail",
|
||||
invalidLink: "http://yourapp.com/invalidLink",
|
||||
invalidVerificationLink: "http://yourapp.com/invalidVerificationLink",
|
||||
choosePassword: "http://yourapp.com/choosePassword"
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Using environment variables to configure Parse Server
|
||||
|
||||
@@ -369,7 +313,7 @@ PARSE_SERVER_APPLICATION_ID
|
||||
PARSE_SERVER_MASTER_KEY
|
||||
PARSE_SERVER_DATABASE_URI
|
||||
PARSE_SERVER_URL
|
||||
PARSE_SERVER_CLOUD
|
||||
PARSE_SERVER_CLOUD_CODE_MAIN
|
||||
```
|
||||
|
||||
The default port is 1337, to use a different port set the PORT environment variable:
|
||||
@@ -378,482 +322,45 @@ The default port is 1337, to use a different port set the PORT environment varia
|
||||
$ PORT=8080 parse-server --appId APPLICATION_ID --masterKey MASTER_KEY
|
||||
```
|
||||
|
||||
For the full list of configurable environment variables, run `parse-server --help` or take a look at [Parse Server Configuration](https://github.com/parse-community/parse-server/blob/master/src/Options/Definitions.js).
|
||||
For the full list of configurable environment variables, run `parse-server --help`.
|
||||
|
||||
### Available Adapters
|
||||
|
||||
All official adapters are distributed as scoped pacakges on [npm (@parse)](https://www.npmjs.com/search?q=scope%3Aparse).
|
||||
|
||||
Some well maintained adapters are also available on the [Parse Server Modules](https://github.com/parse-server-modules) organization.
|
||||
|
||||
You can also find more adapters maintained by the community by searching on [npm](https://www.npmjs.com/search?q=parse-server%20adapter&page=1&ranking=optimal).
|
||||
[Parse Server Modules (Adapters)](https://github.com/parse-server-modules)
|
||||
|
||||
### Configuring File Adapters
|
||||
|
||||
Parse Server allows developers to choose from several options when hosting files:
|
||||
|
||||
* `GridFSBucketAdapter`, which is backed by MongoDB;
|
||||
* `GridStoreAdapter`, which is backed by MongoDB;
|
||||
* `S3Adapter`, which is backed by [Amazon S3](https://aws.amazon.com/s3/); or
|
||||
* `GCSAdapter`, which is backed by [Google Cloud Storage](https://cloud.google.com/storage/)
|
||||
|
||||
`GridFSBucketAdapter` is used by default and requires no setup, but if you're interested in using S3 or Google Cloud Storage, additional configuration information is available in the [Parse Server guide](http://docs.parseplatform.org/parse-server/guide/#configuring-file-adapters).
|
||||
`GridStoreAdapter` is used by default and requires no setup, but if you're interested in using S3 or Google Cloud Storage, additional configuration information is available in the [Parse Server wiki](https://github.com/ParsePlatform/parse-server/wiki/Configuring-File-Adapters).
|
||||
|
||||
### Idempodency Enforcement
|
||||
|
||||
**Caution, this is an experimental feature that may not be appropriate for production.**
|
||||
# Support
|
||||
|
||||
This feature deduplicates identical requests that are received by Parse Server mutliple times, typically due to network issues or network adapter access restrictions on mobile operating systems.
|
||||
For implementation related questions or technical support, please refer to the [Stack Overflow](http://stackoverflow.com/questions/tagged/parse.com) and [Server Fault](https://serverfault.com/tags/parse) communities.
|
||||
|
||||
Identical requests are identified by their request header `X-Parse-Request-Id`. Therefore a client request has to include this header for deduplication to be applied. Requests that do not contain this header cannot be deduplicated and are processed normally by Parse Server. This means rolling out this feature to clients is seamless as Parse Server still processes request without this header when this feature is enbabled.
|
||||
If you believe you've found an issue with Parse Server, make sure these boxes are checked before [reporting an issue](https://github.com/ParsePlatform/parse-server/issues):
|
||||
|
||||
> This feature needs to be enabled on the client side to send the header and on the server to process the header. Refer to the specific Parse SDK docs to see whether the feature is supported yet.
|
||||
- [ ] You've met the [prerequisites](https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#prerequisites).
|
||||
|
||||
Deduplication is only done for object creation and update (`POST` and `PUT` requests). Deduplication is not done for object finding and deletion (`GET` and `DELETE` requests), as these operations are already idempotent by definition.
|
||||
- [ ] You're running the [latest version](https://github.com/ParsePlatform/parse-server/releases) of Parse Server.
|
||||
|
||||
#### Configuration example
|
||||
```
|
||||
let api = new ParseServer({
|
||||
idempotencyOptions: {
|
||||
paths: [".*"], // enforce for all requests
|
||||
ttl: 120 // keep request IDs for 120s
|
||||
}
|
||||
}
|
||||
```
|
||||
#### Parameters
|
||||
|
||||
| Parameter | Optional | Type | Default value | Example values | Environment variable | Description |
|
||||
|-----------|----------|--------|---------------|-----------|-----------|-------------|
|
||||
| `idempotencyOptions` | yes | `Object` | `undefined` | | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_OPTIONS | Setting this enables idempotency enforcement for the specified paths. |
|
||||
| `idempotencyOptions.paths`| yes | `Array<String>` | `[]` | `.*` (all paths, includes the examples below), <br>`functions/.*` (all functions), <br>`jobs/.*` (all jobs), <br>`classes/.*` (all classes), <br>`functions/.*` (all functions), <br>`users` (user creation / update), <br>`installations` (installation creation / update) | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_PATHS | An array of path patterns that have to match the request path for request deduplication to be enabled. The mount path must not be included, for example to match the request path `/parse/functions/myFunction` specifiy the path pattern `functions/myFunction`. A trailing slash of the request path is ignored, for example the path pattern `functions/myFunction` matches both `/parse/functions/myFunction` and `/parse/functions/myFunction/`. |
|
||||
| `idempotencyOptions.ttl` | yes | `Integer` | `300` | `60` (60 seconds) | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_TTL | The duration in seconds after which a request record is discarded from the database. Duplicate requests due to network issues can be expected to arrive within milliseconds up to several seconds. This value must be greater than `0`. |
|
||||
|
||||
#### Notes
|
||||
|
||||
- This feature is currently only available for MongoDB and not for Postgres.
|
||||
|
||||
### Logging
|
||||
|
||||
Parse Server will, by default, log:
|
||||
* to the console
|
||||
* daily rotating files as new line delimited JSON
|
||||
|
||||
Logs are also viewable in Parse Dashboard.
|
||||
|
||||
**Want to log each request and response?** Set the `VERBOSE` environment variable when starting `parse-server`. Usage :- `VERBOSE='1' parse-server --appId APPLICATION_ID --masterKey MASTER_KEY`
|
||||
|
||||
**Want logs to be in placed in a different folder?** Pass the `PARSE_SERVER_LOGS_FOLDER` environment variable when starting `parse-server`. Usage :- `PARSE_SERVER_LOGS_FOLDER='<path-to-logs-folder>' parse-server --appId APPLICATION_ID --masterKey MASTER_KEY`
|
||||
|
||||
**Want to log specific levels?** Pass the `logLevel` parameter when starting `parse-server`. Usage :- `parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --logLevel LOG_LEVEL`
|
||||
|
||||
**Want new line delimited JSON error logs (for consumption by CloudWatch, Google Cloud Logging, etc)?** Pass the `JSON_LOGS` environment variable when starting `parse-server`. Usage :- `JSON_LOGS='1' parse-server --appId APPLICATION_ID --masterKey MASTER_KEY`
|
||||
|
||||
# Live Queries
|
||||
|
||||
Live queries are meant to be used in real-time reactive applications, where just using the traditional query paradigm could cause several problems, like increased response time and high network and server usage. Live queries should be used in cases where you need to continuously update a page with fresh data coming from the database, which often happens in (but is not limited to) online games, messaging clients and shared to-do lists.
|
||||
|
||||
Take a look at [Live Query Guide](https://docs.parseplatform.org/parse-server/guide/#live-queries), [Live Query Server Setup Guide](https://docs.parseplatform.org/parse-server/guide/#scalability) and [Live Query Protocol Specification](https://github.com/parse-community/parse-server/wiki/Parse-LiveQuery-Protocol-Specification). You can setup a standalone server or multiple instances for scalability (recommended).
|
||||
|
||||
# GraphQL
|
||||
|
||||
[GraphQL](https://graphql.org/), developed by Facebook, is an open-source data query and manipulation language for APIs. In addition to the traditional REST API, Parse Server automatically generates a GraphQL API based on your current application schema. Parse Server also allows you to define your custom GraphQL queries and mutations, whose resolvers can be bound to your cloud code functions.
|
||||
|
||||
## Running
|
||||
|
||||
### Using the CLI
|
||||
|
||||
The easiest way to run the Parse GraphQL API is through the CLI:
|
||||
|
||||
```bash
|
||||
$ npm install -g parse-server mongodb-runner
|
||||
$ mongodb-runner start
|
||||
$ parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --databaseURI mongodb://localhost/test --publicServerURL http://localhost:1337/parse --mountGraphQL --mountPlayground
|
||||
```
|
||||
|
||||
After starting the server, you can visit http://localhost:1337/playground in your browser to start playing with your GraphQL API.
|
||||
|
||||
***Note:*** Do ***NOT*** use --mountPlayground option in production. [Parse Dashboard](https://github.com/parse-community/parse-dashboard) has a built-in GraphQL Playground and it is the recommended option for production apps.
|
||||
|
||||
### Using Docker
|
||||
|
||||
You can also run the Parse GraphQL API inside a Docker container:
|
||||
|
||||
```bash
|
||||
$ git clone https://github.com/parse-community/parse-server
|
||||
$ cd parse-server
|
||||
$ docker build --tag parse-server .
|
||||
$ docker run --name my-mongo -d mongo
|
||||
```
|
||||
|
||||
#### Running the Parse Server Image
|
||||
|
||||
```bash
|
||||
$ docker run --name my-parse-server --link my-mongo:mongo -v config-vol:/parse-server/config -p 1337:1337 -d parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --databaseURI mongodb://mongo/test --publicServerURL http://localhost:1337/parse --mountGraphQL --mountPlayground
|
||||
```
|
||||
|
||||
***Note:*** *If you want to use [Cloud Code](https://docs.parseplatform.org/cloudcode/guide/) feature, please add `-v cloud-code-vol:/parse-server/cloud --cloud /parse-server/cloud/main.js` to command above. Make sure the `main.js` file is available in the `cloud-code-vol` directory before run this command. Otherwise, an error will occur.*
|
||||
|
||||
After starting the server, you can visit http://localhost:1337/playground in your browser to start playing with your GraphQL API.
|
||||
|
||||
***Note:*** Do ***NOT*** use --mountPlayground option in production. [Parse Dashboard](https://github.com/parse-community/parse-dashboard) has a built-in GraphQL Playground and it is the recommended option for production apps.
|
||||
|
||||
### Using Express.js
|
||||
|
||||
You can also mount the GraphQL API in an Express.js application together with the REST API or solo. You first need to create a new project and install the required dependencies:
|
||||
|
||||
```bash
|
||||
$ mkdir my-app
|
||||
$ cd my-app
|
||||
$ npm install parse-server express --save
|
||||
```
|
||||
|
||||
Then, create an `index.js` file with the following content:
|
||||
|
||||
```js
|
||||
const express = require('express');
|
||||
const { default: ParseServer, ParseGraphQLServer } = require('parse-server');
|
||||
|
||||
const app = express();
|
||||
|
||||
const parseServer = new ParseServer({
|
||||
databaseURI: 'mongodb://localhost:27017/test',
|
||||
appId: 'APPLICATION_ID',
|
||||
masterKey: 'MASTER_KEY',
|
||||
serverURL: 'http://localhost:1337/parse',
|
||||
publicServerURL: 'http://localhost:1337/parse'
|
||||
});
|
||||
|
||||
const parseGraphQLServer = new ParseGraphQLServer(
|
||||
parseServer,
|
||||
{
|
||||
graphQLPath: '/graphql',
|
||||
playgroundPath: '/playground'
|
||||
}
|
||||
);
|
||||
|
||||
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
|
||||
|
||||
app.listen(1337, function() {
|
||||
console.log('REST API running on http://localhost:1337/parse');
|
||||
console.log('GraphQL API running on http://localhost:1337/graphql');
|
||||
console.log('GraphQL Playground running on http://localhost:1337/playground');
|
||||
});
|
||||
```
|
||||
|
||||
And finally start your app:
|
||||
|
||||
```bash
|
||||
$ npx mongodb-runner start
|
||||
$ node index.js
|
||||
```
|
||||
|
||||
After starting the app, you can visit http://localhost:1337/playground in your browser to start playing with your GraphQL API.
|
||||
|
||||
***Note:*** Do ***NOT*** mount the GraphQL Playground in production. [Parse Dashboard](https://github.com/parse-community/parse-dashboard) has a built-in GraphQL Playground and it is the recommended option for production apps.
|
||||
|
||||
## Checking the API health
|
||||
|
||||
Run the following:
|
||||
|
||||
```graphql
|
||||
query Health {
|
||||
health
|
||||
}
|
||||
```
|
||||
|
||||
You should receive the following response:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"health": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Creating your first class
|
||||
|
||||
Since your application does not have any schema yet, you can use the `createClass` mutation to create your first class. Run the following:
|
||||
|
||||
```graphql
|
||||
mutation CreateClass {
|
||||
createClass(
|
||||
name: "GameScore"
|
||||
schemaFields: {
|
||||
addStrings: [{ name: "playerName" }]
|
||||
addNumbers: [{ name: "score" }]
|
||||
addBooleans: [{ name: "cheatMode" }]
|
||||
}
|
||||
) {
|
||||
name
|
||||
schemaFields {
|
||||
name
|
||||
__typename
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You should receive the following response:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"createClass": {
|
||||
"name": "GameScore",
|
||||
"schemaFields": [
|
||||
{
|
||||
"name": "objectId",
|
||||
"__typename": "SchemaStringField"
|
||||
},
|
||||
{
|
||||
"name": "updatedAt",
|
||||
"__typename": "SchemaDateField"
|
||||
},
|
||||
{
|
||||
"name": "createdAt",
|
||||
"__typename": "SchemaDateField"
|
||||
},
|
||||
{
|
||||
"name": "playerName",
|
||||
"__typename": "SchemaStringField"
|
||||
},
|
||||
{
|
||||
"name": "score",
|
||||
"__typename": "SchemaNumberField"
|
||||
},
|
||||
{
|
||||
"name": "cheatMode",
|
||||
"__typename": "SchemaBooleanField"
|
||||
},
|
||||
{
|
||||
"name": "ACL",
|
||||
"__typename": "SchemaACLField"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Using automatically generated operations
|
||||
|
||||
Parse Server learned from the first class that you created and now you have the `GameScore` class in your schema. You can now start using the automatically generated operations!
|
||||
|
||||
Run the following to create your first object:
|
||||
|
||||
```graphql
|
||||
mutation CreateGameScore {
|
||||
createGameScore(
|
||||
fields: {
|
||||
playerName: "Sean Plott"
|
||||
score: 1337
|
||||
cheatMode: false
|
||||
}
|
||||
) {
|
||||
id
|
||||
updatedAt
|
||||
createdAt
|
||||
playerName
|
||||
score
|
||||
cheatMode
|
||||
ACL
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You should receive a response similar to this:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"createGameScore": {
|
||||
"id": "XN75D94OBD",
|
||||
"updatedAt": "2019-09-17T06:50:26.357Z",
|
||||
"createdAt": "2019-09-17T06:50:26.357Z",
|
||||
"playerName": "Sean Plott",
|
||||
"score": 1337,
|
||||
"cheatMode": false,
|
||||
"ACL": null
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can also run a query to this new class:
|
||||
|
||||
```graphql
|
||||
query GameScores {
|
||||
gameScores {
|
||||
results {
|
||||
id
|
||||
updatedAt
|
||||
createdAt
|
||||
playerName
|
||||
score
|
||||
cheatMode
|
||||
ACL
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You should receive a response similar to this:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"gameScores": {
|
||||
"results": [
|
||||
{
|
||||
"id": "XN75D94OBD",
|
||||
"updatedAt": "2019-09-17T06:50:26.357Z",
|
||||
"createdAt": "2019-09-17T06:50:26.357Z",
|
||||
"playerName": "Sean Plott",
|
||||
"score": 1337,
|
||||
"cheatMode": false,
|
||||
"ACL": null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Customizing your GraphQL Schema
|
||||
|
||||
Parse GraphQL Server allows you to create a custom GraphQL schema with own queries and mutations to be merged with the auto-generated ones. You can resolve these operations using your regular cloud code functions.
|
||||
|
||||
To start creating your custom schema, you need to code a `schema.graphql` file and initialize Parse Server with `--graphQLSchema` and `--cloud` options:
|
||||
|
||||
```bash
|
||||
$ parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --databaseURI mongodb://localhost/test --publicServerURL http://localhost:1337/parse --cloud ./cloud/main.js --graphQLSchema ./cloud/schema.graphql --mountGraphQL --mountPlayground
|
||||
```
|
||||
|
||||
### Creating your first custom query
|
||||
|
||||
Use the code below for your `schema.graphql` and `main.js` files. Then restart your Parse Server.
|
||||
|
||||
```graphql
|
||||
# schema.graphql
|
||||
extend type Query {
|
||||
hello: String! @resolve
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
// main.js
|
||||
Parse.Cloud.define('hello', async () => {
|
||||
return 'Hello world!';
|
||||
});
|
||||
```
|
||||
|
||||
You can now run your custom query using GraphQL Playground:
|
||||
|
||||
```graphql
|
||||
query {
|
||||
hello
|
||||
}
|
||||
```
|
||||
|
||||
You should receive the response below:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"hello": "Hello world!"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Learning more
|
||||
|
||||
The [Parse GraphQL Guide](http://docs.parseplatform.org/graphql/guide/) is a very good source for learning how to use the Parse GraphQL API.
|
||||
|
||||
You also have a very powerful tool inside your GraphQL Playground. Please look at the right side of your GraphQL Playground. You will see the `DOCS` and `SCHEMA` menus. They are automatically generated by analyzing your application schema. Please refer to them and learn more about everything that you can do with your Parse GraphQL API.
|
||||
|
||||
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 3.0.0
|
||||
|
||||
Starting 3.0.0, parse-server uses the JS SDK version 2.0.
|
||||
In short, parse SDK v2.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), hoping this will help you transition to the next major release.
|
||||
- [ ] You've searched through [existing issues](https://github.com/ParsePlatform/parse-server/issues?utf8=%E2%9C%93&q=). Chances are that your issue has been reported or resolved before.
|
||||
|
||||
# Want to ride the bleeding edge?
|
||||
|
||||
It is recommend to use builds deployed npm for many reasons, but if you want to use
|
||||
The `latest` branch in this repository is automatically maintained to be the last
|
||||
commit to `master` to pass all tests, in the same form found on npm. It is
|
||||
recommend to use builds deployed npm for many reasons, but if you want to use
|
||||
the latest not-yet-released version of parse-server, you can do so by depending
|
||||
directly on this branch:
|
||||
|
||||
```
|
||||
npm install parse-community/parse-server.git#master
|
||||
npm install parseplatform/parse-server.git#latest
|
||||
```
|
||||
|
||||
## Experimenting
|
||||
|
||||
You can also use your own forks, and work in progress branches by specifying them:
|
||||
|
||||
```
|
||||
npm install github:myUsername/parse-server#my-awesome-feature
|
||||
```
|
||||
|
||||
And don't forget, if you plan to deploy it remotely, you should run `npm install` with the `--save` option.
|
||||
|
||||
# Contributing
|
||||
|
||||
We really want Parse to be yours, to see it grow and thrive in the open source community. Please see the [Contributing to Parse Server guide](CONTRIBUTING.md).
|
||||
|
||||
# Contributors
|
||||
|
||||
This project exists thanks to all the people who contribute... we'd love to see your face on this list!
|
||||
|
||||
<a href="../../graphs/contributors"><img src="https://opencollective.com/parse-server/contributors.svg?width=890&button=false" /></a>
|
||||
|
||||
# Sponsors
|
||||
|
||||
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [Become a sponsor!](https://opencollective.com/parse-server#sponsor)
|
||||
|
||||
<a href="https://opencollective.com/parse-server/sponsor/0/website" target="_blank"><img src="https://opencollective.com/parse-server/sponsor/0/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/parse-server/sponsor/1/website" target="_blank"><img src="https://opencollective.com/parse-server/sponsor/1/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/parse-server/sponsor/2/website" target="_blank"><img src="https://opencollective.com/parse-server/sponsor/2/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/parse-server/sponsor/3/website" target="_blank"><img src="https://opencollective.com/parse-server/sponsor/3/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/parse-server/sponsor/4/website" target="_blank"><img src="https://opencollective.com/parse-server/sponsor/4/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/parse-server/sponsor/5/website" target="_blank"><img src="https://opencollective.com/parse-server/sponsor/5/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/parse-server/sponsor/6/website" target="_blank"><img src="https://opencollective.com/parse-server/sponsor/6/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/parse-server/sponsor/7/website" target="_blank"><img src="https://opencollective.com/parse-server/sponsor/7/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/parse-server/sponsor/8/website" target="_blank"><img src="https://opencollective.com/parse-server/sponsor/8/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/parse-server/sponsor/9/website" target="_blank"><img src="https://opencollective.com/parse-server/sponsor/9/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/parse-server/sponsor/10/website" target="_blank"><img src="https://opencollective.com/parse-server/sponsor/10/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/parse-server/sponsor/11/website" target="_blank"><img src="https://opencollective.com/parse-server/sponsor/11/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/parse-server/sponsor/12/website" target="_blank"><img src="https://opencollective.com/parse-server/sponsor/12/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/parse-server/sponsor/13/website" target="_blank"><img src="https://opencollective.com/parse-server/sponsor/13/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/parse-server/sponsor/14/website" target="_blank"><img src="https://opencollective.com/parse-server/sponsor/14/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/parse-server/sponsor/15/website" target="_blank"><img src="https://opencollective.com/parse-server/sponsor/15/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/parse-server/sponsor/16/website" target="_blank"><img src="https://opencollective.com/parse-server/sponsor/16/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/parse-server/sponsor/17/website" target="_blank"><img src="https://opencollective.com/parse-server/sponsor/17/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/parse-server/sponsor/18/website" target="_blank"><img src="https://opencollective.com/parse-server/sponsor/18/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/parse-server/sponsor/19/website" target="_blank"><img src="https://opencollective.com/parse-server/sponsor/19/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/parse-server/sponsor/20/website" target="_blank"><img src="https://opencollective.com/parse-server/sponsor/20/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/parse-server/sponsor/21/website" target="_blank"><img src="https://opencollective.com/parse-server/sponsor/21/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/parse-server/sponsor/22/website" target="_blank"><img src="https://opencollective.com/parse-server/sponsor/22/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/parse-server/sponsor/23/website" target="_blank"><img src="https://opencollective.com/parse-server/sponsor/23/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/parse-server/sponsor/24/website" target="_blank"><img src="https://opencollective.com/parse-server/sponsor/24/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/parse-server/sponsor/25/website" target="_blank"><img src="https://opencollective.com/parse-server/sponsor/25/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/parse-server/sponsor/26/website" target="_blank"><img src="https://opencollective.com/parse-server/sponsor/26/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/parse-server/sponsor/27/website" target="_blank"><img src="https://opencollective.com/parse-server/sponsor/27/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/parse-server/sponsor/28/website" target="_blank"><img src="https://opencollective.com/parse-server/sponsor/28/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/parse-server/sponsor/29/website" target="_blank"><img src="https://opencollective.com/parse-server/sponsor/29/avatar.svg"></a>
|
||||
|
||||
# Backers
|
||||
|
||||
Support us with a monthly donation and help us continue our activities. [Become a backer!](https://opencollective.com/parse-server#backer)
|
||||
|
||||
<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.
|
||||
|
||||
-101
@@ -1,101 +0,0 @@
|
||||
# Parse Community Vulnerability Disclosure Program
|
||||
If you believe you have found a security vulnerability on one of parse-community maintained packages,
|
||||
we encourage you to let us know right away.
|
||||
We will investigate all legitimate reports and do our best to quickly fix the problem.
|
||||
Before making a report, please review this page to understand our disclosure policy and how to communicate with us.
|
||||
|
||||
# Responsible Disclosure Policy
|
||||
If you comply with the policies below when reporting a security issue to parse community,
|
||||
we will not initiate a lawsuit or law enforcement investigation against you in response to your report.
|
||||
We ask that:
|
||||
|
||||
- You give us reasonable time to investigate and mitigate an issue you report before making public any information about the report or sharing such information with others. This means we request _at least_ **7 days** to get back to you with an initial response and _at least_ **30 days** from initial contact (made by you) to apply a patch.
|
||||
- You do not interact with an individual account (which includes modifying or accessing data from the account) if the account owner has not consented to such actions.
|
||||
- You make a good faith effort to avoid privacy violations and disruptions to others, including (but not limited to) destruction of data and interruption or degradation of our services.
|
||||
- You do not exploit a security issue you discover for any reason. (This includes demonstrating additional risk, such as attempted compromise of sensitive company data or probing for additional issues). You do not violate any other applicable laws or regulations.
|
||||
|
||||
# Communicating with us
|
||||
|
||||
All vulnerabilities should be privately reported to either [Node Security](https://nodesecurity.io/report) or directly to us at the following address [security at parseplatform dot org](mailto:security@parseplatform.org)
|
||||
|
||||
You can use our PGP public key, which is also uploaded [here](hkp://pgp.mit.edu):
|
||||
|
||||
```
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQINBFbZHTcBEADMJledXkBantsiKc5fbln3j+Bj3R2fP6xcUZ4N6RdKj/19G8e4
|
||||
+Lwso/SEDlKKuh+1ORHrcXbYBPNRTi+syf0dtL6uqNKVS+jzuS48qd7G04Foe+qs
|
||||
rg5k80TfRLboCoESIS4C8E6sdjCMKEj8b+QQU8YyzL470+gYwgg7bfvHyECuS4AD
|
||||
lPssBi03cQdVlYjxNWQZAfVMZ+5zcvpS4P5KOCZPT082rzlgQEmVpmNuTyBELNtl
|
||||
TBcVK9Sq6/KlNNSXMbGfJlMMq0kgAzVxrSyx3y0gOnRx1DR+a5jJSecPtdVJYno8
|
||||
9mwRT6Z1B/boN6GmEhC3vikmsOmA+umaLoscQcwjQj7jK5rPTF8ypuDfVNa+kAUS
|
||||
ONFrayDQljwMEVHZ5/lk9TfEwrnarN8q0fRs2MXaJsD/YlTHG5/9LJs3mMk5yQpq
|
||||
VGq0sydprnubW36nbP0SkH2LMRrLhQWoLEvtjkz7EaqGLWKO6N0Nr+BT1YBy5gM+
|
||||
evc5mUeHUTPqflDht1crHn0rdfWmtDzEsNUWc9GR1hK2+x8U43YUPDmmgRYZyCGP
|
||||
iKdmrF0kUDlh2mmok3dXlQCZesXaeFvSbIFMfL7midhbiWyCfDtAIQPfBTKNtfc3
|
||||
qbaAoEHmYS2Yjri0rRqK9zbFqDgOR7Ap/ExeoOuaAMx1bvjV0QBm0W8q+QARAQAB
|
||||
tC1BcnRodXIgQ2luYWRlciAoR2l0aHViKSA8YXJ0aHVyQHBvcHN1Z2FyLmNvbT6J
|
||||
Aj0EEwEKACcFAloYZqECGwMFCQeGH4AFCwkIBwMFFQoJCAsFFgIDAQACHgECF4AA
|
||||
CgkQgZHETYyfyECCKA/8CbpKrMJn+UhP4s5eUisx6wSfqDWuHGkvhecxTWLRGGRT
|
||||
yycDm7PJxSb3AdJ//sUTGemG88kpLXmEGt3HpINqB0B4J+aqTB/Ei0+1g/FH0LXP
|
||||
RlCehH0RpLHJmplkEbd2VZ8wFN9+tW1u4jhG+LCZD8pAVy7f36QixCZA3fdlt9GN
|
||||
K2Jq2456dMpHmaLdUbrYERcDSKmDVKBRa8/CTe9hAkA83kAt0xgWjr/Byxw+L3wi
|
||||
Ar4/twAwLAHCzl7HTVvbWOXYehM8dpybE7rFV/1OACg3i2uppLE1oGeS2s4HBv84
|
||||
WYNx0oBlBzEefpDAxz1NQI4HnKtBopt8jNUs5GEa1GR4eSNdMf9SmX7MRBNgDKuY
|
||||
PsvZQLUBqG8GYZR214NzK9wf0VkQDkZ+PwG+L5pnpKtc7RwsR49z2qyti/nZfPP7
|
||||
y9gJanTNPkzgx2YAk+UBrKL7435XfFAW6mo2y5LLbD6ouT2hGDfnhsSuMrS4bAdM
|
||||
7ua9B8vs2cnwYXUFM7ydAueaPvfP0x5i0ZQrphls3ZUpKRpWORSXa0fTNinSpzqW
|
||||
YzTmPxJsHsyioPlRsl2/r97I9XJ9i5gjMDkNI3TQpGKFy/YNMk7rkk1dp3hq3aP/
|
||||
xt0P/2yL/MJEj9Jus9FTKGqVtOn73e8oSOsu0ngpllYasYaLkO19MJ2lemSW+CC0
|
||||
LEFydGh1ciBDaW5hZGVyIChHaXRodWIpIDxhY2luYWRlckBnbWFpbC5jb20+iQI9
|
||||
BBMBCgAnBQJaGGJcAhsDBQkHhh+ABQsJCAcDBRUKCQgLBRYCAwEAAh4BAheAAAoJ
|
||||
EIGRxE2Mn8hAGVcP/RkqkER8/AKWfPFQs40Epe3mocuLyEW1CHX5LkFTjya42GAM
|
||||
0BKk+bStRrMQ4rBGOmdKGxphysQFZn4bscRUVMmJd/frJ0s8ConSfWzaweL7rbQO
|
||||
UgGnL4mSNUvQkNCoO/RgKJapq9G/+jA9RRYEoSncE1/i3FQ+96JWfRRYy3MGYi9H
|
||||
WmH3UFQ8cJ1JAFMIGaxuHuNQ20mStVDSuK3Zm8KVxk8rWHb2O8lye4bcBi7OLXYx
|
||||
oZEoLrbLQinMbuccNaMq2j3ZNLOPYUDyyv5O81WzN70A7r0rkipOaJx4LiXE2/NT
|
||||
3vz1CyT7i+2/GlLL113DP0DA8neMjx6MzpxOo7MgT+ZBHRRZh+tWoqfJKclh6Duw
|
||||
rAJ9BOxSCm1y4BxTxuWrb5mU/RDCe3oC7PTA6wIMbJThqxtRpjqa17oWn2UXyJOH
|
||||
aEXvt6jH6YqqFV9liArwkjZZl4KKyiqZ8UFKLteIVSK5xlwQ/ICW3uPYRpYhIFj0
|
||||
fMaqN5SFcMOxtD4L5SP4k7HRn8l/gVoWQyIMJMip87sPCw7mRe5jq91n9s33stHr
|
||||
vByL0ownS5MmvKXLLAyAltw2FcIyafcn6mKNGMUBunM14/j5uXaMcgz3MQtYjkvk
|
||||
Fh6uX1OqLt/rpOhsRTeDRvjGvAFtdLt1QtDEz4i9kGN4h4B/XqwEbVNMWyv4tDlB
|
||||
cnRodXIgQ2luYWRlciA8NzAwNTcyK2FjaW5hZGVyQHVzZXJzLm5vcmVwbHkuZ2l0
|
||||
aHViLmNvbT6JAj0EEwEKACcFAloYaaMCGwMFCQeGH4AFCwkIBwMFFQoJCAsFFgID
|
||||
AQACHgECF4AACgkQgZHETYyfyEANHBAAuOkRMEoCuRjN3Dz/bP7SpWSFnBjOWW42
|
||||
Lbie3bXbT1SYRltd7AM3ICu2M8OzjATzrDimmGi7K4qxFIGnz+sjp9NRr6x7Ohgi
|
||||
bPwmU1OMIjuARPhsauUyyUNI+wKbRG9/tO0YxOUBadsKcVYY+6JxhsjrO5qb9NUI
|
||||
WaNvwfCPlSBDcvsKCOVu6weyw9FGpaaKZcscge8tPPEQCf7FYKy6NYPVK6/D7qn6
|
||||
myaKe/dh/HwozZ0o2NhW3uIAdd4OIvmWE7rh97B7afKXTiIfiqWqtkFhH0RxdR2q
|
||||
Damg0BiGjdARqSnneLKDPgIwr904yM1RD36BkPcP8WH3ommsK95mrUKrZtLAQA6J
|
||||
J6uESkuHNtcy5XTx4eF2cD2uaJTcRjlbAHFBMEI/+vr4umo+8wt38JhY+XtSot6W
|
||||
rS99JU6Ht1/SMYdz/rFisOWHb6hS69DOSCEK68lne6n0u1AnsWnDHwbQxcaSEreR
|
||||
axXMzgMtRuM5R4ncLpx0nUwhxlRoIyo2GN6aghXcCrZt1fsLXBilag1moxZgh+YE
|
||||
RaVOsBASuqO/5m609Mi8AGLbuLU+39Ekb/b2ozw/MRvGPNfXC1XIqPe4asEE9GNL
|
||||
XdVqvrHhEexBpv7El9yQ9qyllzEEdv5+soMcUQmjJAVabx+0gtLb5x3QHD4V8ttT
|
||||
kA8kUPG5MyO5Ag0EVtkdNwEQAKssJS3MZiu6WkBact/HvDjJrq+S1HcxeTLYbFXK
|
||||
lEsolW5sw0IX5ORM9+Z9LfUTyVcyU6w/UbM91IecjNnFQkMvIQy8lVhrqO20FL46
|
||||
Vu6G5HezIf2hg/1vgt891hrKMrySQDDyGo68f6uF3U+SJLeNPRoB4O8qL2RHXfC3
|
||||
3ti6FAoOFfRGe/CNB35viK/L//6O3pCFz/nrckEaMzH/GOrcZ8xlrFyeKhsOjtoR
|
||||
S2MDSNpIJfZP+pbtBgVW5lA5HDlyy5s52jXgd0+1Ktw1FV1uCjsgaX9xfbfXG8o1
|
||||
SxpKpj1dI8WQ/7ZuCTxu0phyJsQPmfIHb5kBvZjm4vqpnCfbbFWxsQE+T01PRsV+
|
||||
rWdh1EG4dlTMkvZtMfAnDZV+Cqf6FELb/KhrbRqlCjHeC99tn6YP9EpvLNIgUnD6
|
||||
qiV2QVHMKZ+wRfRUAYUBtvbFYqbbEqLySpW0ahPB/UmLUMjvArzrQkxvKFM20nb8
|
||||
HnAAKAZpgjhXTO9OBiNErCfiORooZLEs1MBeR1u8932GL/uKSDX0RhTYBBFDVoNy
|
||||
zGj3lW3YfnCurVIjCoj+jAZGMSVi67GnUuhm0Vj2K4mdSbq40TwhXxKlp8G0uSU4
|
||||
SmCm+yjTVcgQj+Xj+fsFJh6YGIgkcLEpbZS6kCLKcnx+44U3nZYPZch0+3/m8Uaf
|
||||
i3e5ABEBAAGJAiUEGAEKAA8FAlbZHTcCGwwFCQeGH4AACgkQgZHETYyfyEBEcQ/8
|
||||
DXyIYahE4JmY4REkdSnTQQ09etNmlqZbnMo1y7aYqDgqoixGpZAyE5U3oxGMeNBD
|
||||
P+XEaZGDav9wfiOlnofMXBa65kbtWoz/+dLc+sTAjNdWvucuzP0yiE0+RNkOtvmY
|
||||
5BlGgIQS9PTRaw86aRFOE5LilAoR/jv+mOMPt1dcLfHksmCpW+3OzPyxCA703fE5
|
||||
l7xOXYOAhPGMco30EftebbZkiaAmoZFese92pRenTJXi007ALhMpjPbk5D7717DZ
|
||||
4/g2gqT+Zs8fZe4tUHjo8LSQrFh/i3TpyBoAIouJsuvVvXy0r+iucKvfBjB4vdQb
|
||||
b33Fft2DYVBMpVVfnjRg1Y+p5IFNWByI5NYfFsf8AWLHhOWargYmiUjHMdDFXuea
|
||||
3QUTzHARp4HsqoZocjhKEoW5+j0MTVM6q7cTGgkNvAUmlPEzpvjQP84zkeM7gskP
|
||||
vaKjgp0gIaCMlzP2fRSKqQ2f84LhKj0mZDy7HQNhtKme1l014HgTbbP7GDJ2UMse
|
||||
uHgdaLLljuHFbHYAgGI7Uck225weDESF8enizh1ZF1itRliN47ICsef1RQJCgrJb
|
||||
dkoPBN52k7VhS3vUIQhA1P1sLSEtPMuJ8SDq0CuA008WpU/xHdm1b+xcBxrabuoz
|
||||
6jfgzgnAZveF5DMisrOnbi4GHVIiHXvWrrIglA6o1sM=
|
||||
=paxU
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
```
|
||||
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
var nodemon = require('nodemon');
|
||||
var babel = require("babel-core");
|
||||
var gaze = require('gaze');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
|
||||
// Watch the src and transpile when changed
|
||||
gaze('src/**/*', function(err, watcher) {
|
||||
if (err) throw err;
|
||||
watcher.on('changed', function(sourceFile) {
|
||||
console.log(sourceFile + " has changed");
|
||||
try {
|
||||
targetFile = path.relative(__dirname, sourceFile).replace(/\/src\//, '/lib/');
|
||||
targetFile = path.resolve(__dirname, targetFile);
|
||||
fs.writeFile(targetFile, babel.transformFileSync(sourceFile).code);
|
||||
} catch (e) {
|
||||
console.error(e.message, e.stack);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
// Run and watch dist
|
||||
nodemon({
|
||||
script: 'bin/parse-server',
|
||||
ext: 'js json',
|
||||
watch: 'lib'
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e.message, e.stack);
|
||||
}
|
||||
|
||||
process.once('SIGINT', function() {
|
||||
process.exit(0);
|
||||
});
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/bin/sh
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
NC='\033[0m'
|
||||
@@ -95,10 +95,10 @@ confirm 'Y' 'Do you want to continue? (Y/n): '
|
||||
check_node
|
||||
check_npm
|
||||
|
||||
printf "Setting up parse-server in %s\n" "${INSTALL_DIR}"
|
||||
printf "Setting up parse-server in %s" "${INSTALL_DIR}\n"
|
||||
|
||||
if [ -d "${INSTALL_DIR}" ]; then
|
||||
echo "${CHECK} ${INSTALL_DIR} exists"
|
||||
echo "{CHECK} ${INSTALL_DIR} exists"
|
||||
else
|
||||
mkdir -p "${INSTALL_DIR}"
|
||||
echo "${CHECK} Created ${INSTALL_DIR}"
|
||||
@@ -163,7 +163,7 @@ cat > ./package.json << EOF
|
||||
"start": "parse-server config.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"parse-server": "^3.9.0"
|
||||
"parse-server": "^2.0.0"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"plugins": ["node_modules/jsdoc-babel", "plugins/markdown"],
|
||||
"babel": {
|
||||
"plugins": ["@babel/plugin-transform-flow-strip-types"]
|
||||
},
|
||||
"source": {
|
||||
"include": ["./README.md", "./src/cloud-code", "./src/Options/docs.js", "./src/ParseServer.js", "./src/Adapters"],
|
||||
"excludePattern": "(^|\\/|\\\\)_"
|
||||
},
|
||||
"templates": {
|
||||
"default": {
|
||||
"outputSourceFiles": false,
|
||||
"showInheritedInNav": false,
|
||||
"useLongnameInNav": true
|
||||
},
|
||||
"cleverLinks": true,
|
||||
"monospaceLinks": false
|
||||
},
|
||||
"opts": {
|
||||
"template": "node_modules/@parse/minami",
|
||||
"recurse": true
|
||||
}
|
||||
}
|
||||
Generated
-17604
File diff suppressed because it is too large
Load Diff
+56
-117
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "parse-server",
|
||||
"version": "4.10.14",
|
||||
"version": "2.3.0",
|
||||
"description": "An express module providing a Parse-compatible API server",
|
||||
"main": "lib/index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/parse-community/parse-server"
|
||||
"url": "https://github.com/ParsePlatform/parse-server"
|
||||
},
|
||||
"files": [
|
||||
"bin/",
|
||||
@@ -14,137 +14,76 @@
|
||||
"views/",
|
||||
"LICENSE",
|
||||
"PATENTS",
|
||||
"postinstall.js",
|
||||
"README.md"
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@apollographql/graphql-playground-html": "1.6.26",
|
||||
"@graphql-tools/links": "6.2.5",
|
||||
"@graphql-tools/stitch": "6.2.4",
|
||||
"@graphql-tools/utils": "6.2.4",
|
||||
"@parse/fs-files-adapter": "1.2.0",
|
||||
"@parse/push-adapter": "4.1.2",
|
||||
"@parse/s3-files-adapter": "1.6.2",
|
||||
"@parse/simple-mailgun-adapter": "1.1.0",
|
||||
"apollo-server-express": "2.19.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"body-parser": "1.19.0",
|
||||
"commander": "5.1.0",
|
||||
"cors": "2.8.5",
|
||||
"deepcopy": "2.1.0",
|
||||
"express": "4.17.1",
|
||||
"follow-redirects": "1.14.8",
|
||||
"graphql": "15.5.1",
|
||||
"graphql-list-fields": "2.0.2",
|
||||
"graphql-relay": "0.7.0",
|
||||
"graphql-tag": "2.12.5",
|
||||
"graphql-upload": "11.0.0",
|
||||
"bcryptjs": "2.3.0",
|
||||
"body-parser": "1.15.2",
|
||||
"commander": "2.9.0",
|
||||
"deepcopy": "0.6.3",
|
||||
"express": "4.14.0",
|
||||
"intersect": "1.0.1",
|
||||
"jsonwebtoken": "8.5.1",
|
||||
"jwks-rsa": "1.12.3",
|
||||
"ldapjs": "2.2.2",
|
||||
"lodash": "4.17.21",
|
||||
"lru-cache": "5.1.1",
|
||||
"mime": "2.4.6",
|
||||
"mongodb": "3.6.11",
|
||||
"parse": "3.3.0",
|
||||
"pg-promise": "10.8.1",
|
||||
"pluralize": "8.0.0",
|
||||
"redis": "3.1.2",
|
||||
"semver": "7.3.2",
|
||||
"subscriptions-transport-ws": "0.10.0",
|
||||
"tv4": "1.3.0",
|
||||
"uuid": "8.3.1",
|
||||
"winston": "3.3.3",
|
||||
"winston-daily-rotate-file": "4.5.0",
|
||||
"ws": "7.5.3"
|
||||
"lodash": "4.17.2",
|
||||
"lru-cache": "4.0.2",
|
||||
"mime": "1.3.4",
|
||||
"mongodb": "2.2.10",
|
||||
"multer": "1.2.0",
|
||||
"parse": "1.9.2",
|
||||
"parse-server-fs-adapter": "1.0.1",
|
||||
"parse-server-push-adapter": "1.1.0",
|
||||
"parse-server-s3-adapter": "1.0.6",
|
||||
"parse-server-simple-mailgun-adapter": "1.0.0",
|
||||
"pg-promise": "5.4.5",
|
||||
"redis": "2.6.3",
|
||||
"request": "2.79.0",
|
||||
"semver": "5.2.0",
|
||||
"tv4": "1.2.7",
|
||||
"winston": "2.3.0",
|
||||
"winston-daily-rotate-file": "1.4.0",
|
||||
"ws": "1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.14.8",
|
||||
"@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",
|
||||
"apollo-cache-inmemory": "1.6.6",
|
||||
"apollo-client": "2.6.10",
|
||||
"apollo-link": "1.2.14",
|
||||
"apollo-link-http": "1.5.17",
|
||||
"apollo-link-ws": "1.0.20",
|
||||
"apollo-upload-client": "13.0.0",
|
||||
"apollo-utilities": "1.3.4",
|
||||
"babel-eslint": "10.1.0",
|
||||
"babel-cli": "6.18.0",
|
||||
"babel-core": "6.18.2",
|
||||
"babel-eslint": "^7.1.1",
|
||||
"babel-plugin-syntax-flow": "6.13.0",
|
||||
"babel-plugin-transform-flow-strip-types": "6.18.0",
|
||||
"babel-preset-es2015": "6.14.0",
|
||||
"babel-preset-stage-0": "6.5.0",
|
||||
"babel-register": "6.18.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.2.5",
|
||||
"jasmine": "3.5.0",
|
||||
"jasmine-spec-reporter": "7.0.0",
|
||||
"jsdoc": "3.6.7",
|
||||
"jsdoc-babel": "0.5.0",
|
||||
"lint-staged": "10.2.3",
|
||||
"mongodb-runner": "mongodb-js/runner",
|
||||
"node-fetch": "2.6.1",
|
||||
"nyc": "15.1.0",
|
||||
"prettier": "2.0.5",
|
||||
"@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",
|
||||
"semantic-release": "17.4.6"
|
||||
"cross-env": "3.1.3",
|
||||
"deep-diff": "0.3.4",
|
||||
"eslint": "^3.10.2",
|
||||
"eslint-plugin-flowtype": "^2.25.0",
|
||||
"gaze": "1.1.1",
|
||||
"istanbul": "1.0.0-alpha.1",
|
||||
"jasmine": "2.5.2",
|
||||
"jasmine-spec-reporter": "^2.7.0",
|
||||
"mongodb-runner": "3.4.0",
|
||||
"nodemon": "1.11.0",
|
||||
"request-promise": "4.1.1"
|
||||
},
|
||||
"scripts": {
|
||||
"definitions": "node ./resources/buildConfigDefinitions.js",
|
||||
"docs": "jsdoc -c ./jsdoc-conf.json",
|
||||
"lint": "flow && eslint --cache ./",
|
||||
"lint-fix": "eslint --fix --cache ./",
|
||||
"build": "babel src/ -d lib/ --copy-files",
|
||||
"watch": "babel --watch src/ -d lib/ --copy-files",
|
||||
"pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=4.0.4} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=mmapv1} mongodb-runner start",
|
||||
"testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=4.0.4} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=mmapv1} TESTING=1 jasmine",
|
||||
"test": "npm run testonly",
|
||||
"posttest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=4.0.4} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=mmapv1} mongodb-runner stop",
|
||||
"coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=4.0.4} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=mmapv1} TESTING=1 nyc jasmine",
|
||||
"dev": "npm run build && node bin/dev",
|
||||
"lint": "eslint --cache ./",
|
||||
"build": "babel src/ -d lib/",
|
||||
"pretest": "npm run lint",
|
||||
"test": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.2.6} MONGODB_STORAGE_ENGINE=mmapv1 NODE_ENV=test TESTING=1 $COVERAGE_OPTION jasmine",
|
||||
"test:win": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.2.6} MONGODB_STORAGE_ENGINE=mmapv1 NODE_ENV=test TESTING=1 jasmine",
|
||||
"coverage": "cross-env COVERAGE_OPTION='./node_modules/.bin/istanbul cover' npm test",
|
||||
"coverage:win": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.2.6} MONGODB_STORAGE_ENGINE=mmapv1 NODE_ENV=test TESTING=1 node ./node_modules/istanbul/lib/cli.js cover ./node_modules/jasmine/bin/jasmine.js",
|
||||
"start": "node ./bin/parse-server",
|
||||
"prettier": "prettier --write '{src,spec}/{**/*,*}.js'",
|
||||
"prepare": "npm run build",
|
||||
"postinstall": "node -p 'require(\"./postinstall.js\")()'"
|
||||
"prepublish": "npm run build"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
"node": ">=4.5"
|
||||
},
|
||||
"bin": {
|
||||
"parse-server": "bin/parse-server"
|
||||
"parse-server": "./bin/parse-server"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@node-rs/bcrypt": "0.4.1"
|
||||
},
|
||||
"collective": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parse-server",
|
||||
"logo": "https://opencollective.com/parse-server/logo.txt?reverse=true&variant=binary"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parse-server"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"{src,spec}/{**/*,*}.js": [
|
||||
"prettier --write",
|
||||
"eslint --fix --cache",
|
||||
"git add"
|
||||
]
|
||||
"bcrypt": "1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
const pkg = require('./package.json');
|
||||
|
||||
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
|
||||
|
||||
|
||||
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);
|
||||
};
|
||||
@@ -35,8 +35,6 @@
|
||||
padding: 0 0 0 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Invalid Link</h1>
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- This page is displayed when someone navigates to a verify email or reset password link
|
||||
but their security token is wrong. This can either mean the user has clicked on a
|
||||
stale link (i.e. re-click on a password reset link after resetting their password) or
|
||||
(rarely) this could be a sign of a malicious user trying to tamper with your app.
|
||||
-->
|
||||
<html>
|
||||
<head>
|
||||
<title>Invalid Link</title>
|
||||
<style type='text/css'>
|
||||
.container {
|
||||
border-width: 0px;
|
||||
display: block;
|
||||
font: inherit;
|
||||
font-family: 'Helvetica Neue', Helvetica;
|
||||
font-size: 16px;
|
||||
height: 30px;
|
||||
line-height: 16px;
|
||||
margin: 45px 0px 0px 45px;
|
||||
padding: 0px 8px 0px 8px;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5 {
|
||||
color: #0067AB;
|
||||
display: block;
|
||||
font: inherit;
|
||||
font-family: 'Open Sans', 'Helvetica Neue', Helvetica;
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
margin: 0 0 15px 0;
|
||||
padding: 0 0 0 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<script type="text/javascript">
|
||||
function getUrlParameter(name) {
|
||||
name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
|
||||
var regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
|
||||
var results = regex.exec(location.search);
|
||||
return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
|
||||
};
|
||||
|
||||
window.onload = addDataToForm;
|
||||
|
||||
function addDataToForm() {
|
||||
var username = getUrlParameter("username");
|
||||
document.getElementById("usernameField").value = username;
|
||||
|
||||
var appId = getUrlParameter("appId");
|
||||
document.getElementById("resendForm").action = '/apps/' + appId + '/resend_verification_email'
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Invalid Verification Link</h1>
|
||||
<form id="resendForm" method="POST" action="/resend_verification_email">
|
||||
<input id="usernameField" class="form-control" name="username" type="hidden" value="">
|
||||
<button type="submit" class="btn btn-default">Resend Link</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,45 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- This page is displayed when someone navigates to a verify email link with an invalid
|
||||
security token and requests a link resend. This page is displayed when the username from
|
||||
the original link is invalid or if the email of that user has already been verfieid when
|
||||
the resend request is made
|
||||
-->
|
||||
<html>
|
||||
<head>
|
||||
<title>Invalid Link</title>
|
||||
<style type='text/css'>
|
||||
.container {
|
||||
border-width: 0px;
|
||||
display: block;
|
||||
font: inherit;
|
||||
font-family: 'Helvetica Neue', Helvetica;
|
||||
font-size: 16px;
|
||||
height: 30px;
|
||||
line-height: 16px;
|
||||
margin: 45px 0px 0px 45px;
|
||||
padding: 0px 8px 0px 8px;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5 {
|
||||
color: #0067AB;
|
||||
display: block;
|
||||
font: inherit;
|
||||
font-family: 'Open Sans', 'Helvetica Neue', Helvetica;
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
margin: 0 0 15px 0;
|
||||
padding: 0 0 0 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>No link sent. User not found or email already verified</h1>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,45 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- This page is displayed when someone navigates to a verify email link with an invalid
|
||||
security token and requests a link resend. This page is displayed when the username
|
||||
from the original verification link has been found and a new verification link has
|
||||
been successfully sent to the corresponding stored email
|
||||
-->
|
||||
<html>
|
||||
<head>
|
||||
<title>Invalid Link</title>
|
||||
<style type='text/css'>
|
||||
.container {
|
||||
border-width: 0px;
|
||||
display: block;
|
||||
font: inherit;
|
||||
font-family: 'Helvetica Neue', Helvetica;
|
||||
font-size: 16px;
|
||||
height: 30px;
|
||||
line-height: 16px;
|
||||
margin: 45px 0px 0px 45px;
|
||||
padding: 0px 8px 0px 8px;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5 {
|
||||
color: #0067AB;
|
||||
display: block;
|
||||
font: inherit;
|
||||
font-family: 'Open Sans', 'Helvetica Neue', Helvetica;
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
margin: 0 0 15px 0;
|
||||
padding: 0 0 0 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Link Sent! Check your email.</h1>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,111 +0,0 @@
|
||||
/**
|
||||
* Semantic Release Config
|
||||
*/
|
||||
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
// Get env vars
|
||||
const ref = process.env.GITHUB_REF;
|
||||
const serverUrl = process.env.GITHUB_SERVER_URL;
|
||||
const repository = process.env.GITHUB_REPOSITORY;
|
||||
const repositoryUrl = serverUrl + '/' + repository;
|
||||
|
||||
// Declare params
|
||||
const resourcePath = './.releaserc/';
|
||||
const templates = {
|
||||
main: { file: 'template.hbs', text: undefined },
|
||||
header: { file: 'header.hbs', text: undefined },
|
||||
commit: { file: 'commit.hbs', text: undefined },
|
||||
footer: { file: 'footer.hbs', text: undefined },
|
||||
};
|
||||
|
||||
// Declare semantic config
|
||||
async function config() {
|
||||
|
||||
// Get branch
|
||||
const branch = ref.split('/').pop();
|
||||
console.log(`Running on branch: ${branch}`);
|
||||
|
||||
// Set changelog file
|
||||
const changelogFile = `./CHANGELOG.md`;
|
||||
console.log(`Changelog file output to: ${changelogFile}`);
|
||||
|
||||
// Load template file contents
|
||||
await loadTemplates();
|
||||
|
||||
const config = {
|
||||
branches: [
|
||||
'release',
|
||||
{ name: 'alpha', prerelease: true },
|
||||
{ name: 'beta', prerelease: true },
|
||||
'next-major',
|
||||
// Long-Term-Support branches
|
||||
{ name: 'release-4.x.x', range: '4.x.x', channel: '4.x.x' },
|
||||
],
|
||||
dryRun: false,
|
||||
debug: true,
|
||||
ci: true,
|
||||
tagFormat: '${version}',
|
||||
plugins: [
|
||||
['@semantic-release/commit-analyzer', {
|
||||
preset: 'angular',
|
||||
releaseRules: [
|
||||
{ type: 'docs', scope: 'README', release: 'patch' },
|
||||
{ scope: 'no-release', release: false },
|
||||
],
|
||||
parserOpts: {
|
||||
noteKeywords: [ 'BREAKING CHANGE', 'BREAKING CHANGES', 'BREAKING' ],
|
||||
},
|
||||
}],
|
||||
['@semantic-release/release-notes-generator', {
|
||||
preset: 'angular',
|
||||
parserOpts: {
|
||||
noteKeywords: ['BREAKING CHANGE', 'BREAKING CHANGES', 'BREAKING']
|
||||
},
|
||||
writerOpts: {
|
||||
commitsSort: ['subject', 'scope'],
|
||||
mainTemplate: templates.main.text,
|
||||
headerPartial: templates.header.text,
|
||||
commitPartial: templates.commit.text,
|
||||
footerPartial: templates.footer.text,
|
||||
},
|
||||
}],
|
||||
['@semantic-release/changelog', {
|
||||
'changelogFile': changelogFile,
|
||||
}],
|
||||
['@semantic-release/npm', {
|
||||
'npmPublish': true,
|
||||
}],
|
||||
['@semantic-release/git', {
|
||||
assets: [changelogFile, 'package.json', 'package-lock.json', 'npm-shrinkwrap.json'],
|
||||
}],
|
||||
['@semantic-release/github', {
|
||||
successComment: getReleaseComment(),
|
||||
labels: ['type:ci'],
|
||||
releasedLabels: ['state:released<%= nextRelease.channel ? `-\${nextRelease.channel}` : "" %>']
|
||||
}],
|
||||
],
|
||||
};
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
async function loadTemplates() {
|
||||
for (const template of Object.keys(templates)) {
|
||||
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 + ')';
|
||||
return comment;
|
||||
}
|
||||
|
||||
module.exports = config();
|
||||
@@ -1,29 +0,0 @@
|
||||
#!/bin/sh -e
|
||||
set -x
|
||||
if [ "${GITHUB_ACTIONS}" = "" ];
|
||||
then
|
||||
echo "Cannot release docs without GITHUB_ACTIONS set"
|
||||
exit 0;
|
||||
fi
|
||||
REPO="https://github.com/parse-community/parse-server"
|
||||
|
||||
rm -rf docs
|
||||
git clone -b gh-pages --single-branch $REPO ./docs
|
||||
cd docs
|
||||
git pull origin gh-pages
|
||||
cd ..
|
||||
|
||||
DEST="master"
|
||||
|
||||
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/${DEST}"
|
||||
cp -R out/* "docs/api/${DEST}"
|
||||
@@ -1,300 +0,0 @@
|
||||
/**
|
||||
* Parse Server Configuration Builder
|
||||
*
|
||||
* This module builds the definitions file (src/Options/Definitions.js)
|
||||
* from the src/Options/index.js options interfaces.
|
||||
* The Definitions.js module is responsible for the default values as well
|
||||
* as the mappings for the CLI.
|
||||
*
|
||||
* To rebuild the definitions file, run
|
||||
* `$ node resources/buildConfigDefinitions.js`
|
||||
*/
|
||||
const parsers = require('../src/Options/parsers');
|
||||
|
||||
function last(array) {
|
||||
return array[array.length - 1];
|
||||
}
|
||||
|
||||
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
function toENV(key) {
|
||||
let str = '';
|
||||
let previousIsUpper = false;
|
||||
for(let i = 0; i < key.length; i++) {
|
||||
const char = key[i];
|
||||
if (letters.indexOf(char) >= 0) {
|
||||
if (!previousIsUpper) {
|
||||
str += '_';
|
||||
previousIsUpper = true;
|
||||
}
|
||||
} else {
|
||||
previousIsUpper = false;
|
||||
}
|
||||
str += char;
|
||||
}
|
||||
return str.toUpperCase();
|
||||
}
|
||||
|
||||
function getCommentValue(comment) {
|
||||
if (!comment) { return }
|
||||
return comment.value.trim();
|
||||
}
|
||||
|
||||
function getENVPrefix(iface) {
|
||||
const options = {
|
||||
'ParseServerOptions' : 'PARSE_SERVER_',
|
||||
'CustomPagesOptions' : 'PARSE_SERVER_CUSTOM_PAGES_',
|
||||
'LiveQueryServerOptions' : 'PARSE_LIVE_QUERY_SERVER_',
|
||||
'LiveQueryOptions' : 'PARSE_SERVER_LIVEQUERY_',
|
||||
'IdempotencyOptions' : 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_',
|
||||
'AccountLockoutOptions' : 'PARSE_SERVER_ACCOUNT_LOCKOUT_',
|
||||
'PasswordPolicyOptions' : 'PARSE_SERVER_PASSWORD_POLICY_'
|
||||
}
|
||||
if (options[iface.id.name]) {
|
||||
return options[iface.id.name]
|
||||
}
|
||||
}
|
||||
|
||||
function processProperty(property, iface) {
|
||||
const firstComment = getCommentValue(last(property.leadingComments || []));
|
||||
const name = property.key.name;
|
||||
const prefix = getENVPrefix(iface);
|
||||
|
||||
if (!firstComment) {
|
||||
return;
|
||||
}
|
||||
const lines = firstComment.split('\n').map((line) => line.trim());
|
||||
let help = '';
|
||||
let envLine;
|
||||
let defaultLine;
|
||||
lines.forEach((line) => {
|
||||
if (line.indexOf(':ENV:') === 0) {
|
||||
envLine = line;
|
||||
} else if (line.indexOf(':DEFAULT:') === 0) {
|
||||
defaultLine = line;
|
||||
} else {
|
||||
help += line;
|
||||
}
|
||||
});
|
||||
let env;
|
||||
if (envLine) {
|
||||
env = envLine.split(' ')[1];
|
||||
} else {
|
||||
env = (prefix + toENV(name));
|
||||
}
|
||||
let defaultValue;
|
||||
if (defaultLine) {
|
||||
defaultValue = defaultLine.split(' ')[1];
|
||||
}
|
||||
let type = property.value.type;
|
||||
let isRequired = true;
|
||||
if (type == 'NullableTypeAnnotation') {
|
||||
isRequired = false;
|
||||
type = property.value.typeAnnotation.type;
|
||||
}
|
||||
return {
|
||||
name,
|
||||
env,
|
||||
help,
|
||||
type,
|
||||
defaultValue,
|
||||
types: property.value.types,
|
||||
typeAnnotation: property.value.typeAnnotation,
|
||||
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);
|
||||
}
|
||||
|
||||
function mapperFor(elt, t) {
|
||||
const p = t.identifier('parsers');
|
||||
const wrap = (identifier) => t.memberExpression(p, identifier);
|
||||
|
||||
if (t.isNumberTypeAnnotation(elt)) {
|
||||
return t.callExpression(wrap(t.identifier('numberParser')), [t.stringLiteral(elt.name)]);
|
||||
} else if (t.isArrayTypeAnnotation(elt)) {
|
||||
return wrap(t.identifier('arrayParser'));
|
||||
} else if (t.isAnyTypeAnnotation(elt)) {
|
||||
return wrap(t.identifier('objectParser'));
|
||||
} else if (t.isBooleanTypeAnnotation(elt)) {
|
||||
return wrap(t.identifier('booleanParser'));
|
||||
} else if (t.isGenericTypeAnnotation(elt)) {
|
||||
const type = elt.typeAnnotation.id.name;
|
||||
if (type == 'Adapter') {
|
||||
return wrap(t.identifier('moduleOrObjectParser'));
|
||||
}
|
||||
if (type == 'NumberOrBoolean') {
|
||||
return wrap(t.identifier('numberOrBooleanParser'));
|
||||
}
|
||||
return wrap(t.identifier('objectParser'));
|
||||
}
|
||||
}
|
||||
|
||||
function parseDefaultValue(elt, value, t) {
|
||||
let literalValue;
|
||||
if (t.isStringTypeAnnotation(elt)) {
|
||||
if (value == '""' || value == "''") {
|
||||
literalValue = t.stringLiteral('');
|
||||
} else {
|
||||
literalValue = t.stringLiteral(value);
|
||||
}
|
||||
} else if (t.isNumberTypeAnnotation(elt)) {
|
||||
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');
|
||||
}
|
||||
}));
|
||||
} else if (t.isAnyTypeAnnotation(elt)) {
|
||||
literalValue = t.arrayExpression([]);
|
||||
} else if (t.isBooleanTypeAnnotation(elt)) {
|
||||
literalValue = t.booleanLiteral(parsers.booleanParser(value));
|
||||
} else if (t.isGenericTypeAnnotation(elt)) {
|
||||
const type = elt.typeAnnotation.id.name;
|
||||
if (type == 'NumberOrBoolean') {
|
||||
literalValue = t.numericLiteral(parsers.numberOrBoolParser('')(value));
|
||||
}
|
||||
if (type == 'CustomPagesOptions') {
|
||||
const object = parsers.objectParser(value);
|
||||
const props = Object.keys(object).map((key) => {
|
||||
return t.objectProperty(key, object[value]);
|
||||
});
|
||||
literalValue = t.objectExpression(props);
|
||||
}
|
||||
if (type == 'IdempotencyOptions') {
|
||||
const object = parsers.objectParser(value);
|
||||
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')]))
|
||||
])
|
||||
);
|
||||
literalValue = t.objectExpression([prop]);
|
||||
}
|
||||
}
|
||||
return literalValue;
|
||||
}
|
||||
|
||||
function inject(t, list) {
|
||||
let comments = '';
|
||||
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]));
|
||||
}
|
||||
}).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} `);
|
||||
}
|
||||
}
|
||||
let type = elt.type.replace('TypeAnnotation', '');
|
||||
if (type === 'Generic') {
|
||||
type = elt.typeAnnotation.id.name;
|
||||
}
|
||||
if (type === 'Array') {
|
||||
type = `${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])
|
||||
}
|
||||
let docs = ``;
|
||||
const plugin = function (babel) {
|
||||
const t = babel.types;
|
||||
const moduleExports = t.memberExpression(t.identifier('module'), t.identifier('exports'));
|
||||
return {
|
||||
visitor: {
|
||||
ImportDeclaration: function(path) {
|
||||
path.remove();
|
||||
},
|
||||
Program: function(path) {
|
||||
// Inject the parser's loader
|
||||
path.unshiftContainer('body', makeRequire('parsers', './parsers', t));
|
||||
},
|
||||
ExportDeclaration: function(path) {
|
||||
// Export declaration on an interface
|
||||
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))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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 });
|
||||
require('fs').writeFileSync('./src/Options/Definitions.js', res.code + '\n');
|
||||
require('fs').writeFileSync('./src/Options/docs.js', docs);
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
docker login -e="$DOCKER_EMAIL" -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
|
||||
docker build -t parseplatform/parse-server:$TRAVIS_TAG .
|
||||
docker push parseplatform/parse-server:$TRAVIS_TAG
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/bin/sh -e
|
||||
|
||||
# This script maintains a git branch which mirrors master but in a form that
|
||||
# what will eventually be deployed to npm, allowing npm dependencies to use:
|
||||
#
|
||||
# "parse-server": "parseplatform/parse-server#latest"
|
||||
#
|
||||
|
||||
# From: https://github.com/graphql/graphql-js/blob/master/resources/npm-git.sh
|
||||
|
||||
BUILD_DIR=latest
|
||||
|
||||
npm run build
|
||||
|
||||
mkdir -p $BUILD_DIR
|
||||
|
||||
cp package.json $BUILD_DIR/
|
||||
cp README.md $BUILD_DIR/
|
||||
cp LICENSE $BUILD_DIR/
|
||||
cp PATENTS $BUILD_DIR/
|
||||
cp CHANGELOG.md $BUILD_DIR/
|
||||
cp -R lib $BUILD_DIR
|
||||
cp -R bin $BUILD_DIR
|
||||
cp -R public_html $BUILD_DIR
|
||||
cp -R views $BUILD_DIR
|
||||
|
||||
cd $BUILD_DIR
|
||||
git init
|
||||
git config user.name "Travis CI"
|
||||
git config user.email "github@fb.com"
|
||||
git add .
|
||||
git commit -m "Deploy master to LATEST branch"
|
||||
git push --force --quiet "https://${GH_TOKEN}@github.com/parseplatform/parse-server.git" master:latest
|
||||
@@ -0,0 +1,102 @@
|
||||
"""
|
||||
https://github.com/dmakhno/travis_after_all/blob/master/travis_after_all.py
|
||||
https://github.com/graphql/graphql-js/blob/master/resources/travis_after_all.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
import logging
|
||||
|
||||
try:
|
||||
import urllib.request as urllib2
|
||||
except ImportError:
|
||||
import urllib2
|
||||
|
||||
log = logging.getLogger("travis.leader")
|
||||
log.addHandler(logging.StreamHandler())
|
||||
log.setLevel(logging.INFO)
|
||||
|
||||
TRAVIS_JOB_NUMBER = 'TRAVIS_JOB_NUMBER'
|
||||
TRAVIS_BUILD_ID = 'TRAVIS_BUILD_ID'
|
||||
POLLING_INTERVAL = 'LEADER_POLLING_INTERVAL'
|
||||
|
||||
build_id = os.getenv(TRAVIS_BUILD_ID)
|
||||
polling_interval = int(os.getenv(POLLING_INTERVAL, '5'))
|
||||
|
||||
#assume, first job is the leader
|
||||
is_leader = lambda job_number: job_number.endswith('.1')
|
||||
|
||||
if not os.getenv(TRAVIS_JOB_NUMBER):
|
||||
# seems even for builds with only one job, this won't get here
|
||||
log.fatal("Don't use defining leader for build without matrix")
|
||||
exit(1)
|
||||
elif is_leader(os.getenv(TRAVIS_JOB_NUMBER)):
|
||||
log.info("This is a leader")
|
||||
else:
|
||||
#since python is subprocess, env variables are exported back via file
|
||||
with open(".to_export_back", "w") as export_var:
|
||||
export_var.write("BUILD_MINION=YES")
|
||||
log.info("This is a minion")
|
||||
exit(0)
|
||||
|
||||
|
||||
class MatrixElement(object):
|
||||
def __init__(self, json_raw):
|
||||
self.is_finished = json_raw['finished_at'] is not None
|
||||
self.is_succeeded = json_raw['result'] == 0
|
||||
self.number = json_raw['number']
|
||||
self.is_leader = is_leader(self.number)
|
||||
|
||||
|
||||
def matrix_snapshot():
|
||||
"""
|
||||
:return: Matrix List
|
||||
"""
|
||||
response = urllib2.build_opener().open("https://api.travis-ci.org/builds/{0}".format(build_id)).read()
|
||||
raw_json = json.loads(response)
|
||||
matrix_without_leader = [MatrixElement(element) for element in raw_json["matrix"]]
|
||||
return matrix_without_leader
|
||||
|
||||
|
||||
def wait_others_to_finish():
|
||||
def others_finished():
|
||||
"""
|
||||
Dumps others to finish
|
||||
Leader cannot finish, it is working now
|
||||
:return: tuple(True or False, List of not finished jobs)
|
||||
"""
|
||||
snapshot = matrix_snapshot()
|
||||
finished = [el.is_finished for el in snapshot if not el.is_leader]
|
||||
return reduce(lambda a, b: a and b, finished), [el.number for el in snapshot if
|
||||
not el.is_leader and not el.is_finished]
|
||||
|
||||
while True:
|
||||
finished, waiting_list = others_finished()
|
||||
if finished: break
|
||||
log.info("Leader waits for minions {0}...".format(waiting_list)) # just in case do not get "silence timeout"
|
||||
time.sleep(polling_interval)
|
||||
|
||||
|
||||
try:
|
||||
wait_others_to_finish()
|
||||
|
||||
final_snapshot = matrix_snapshot()
|
||||
log.info("Final Results: {0}".format([(e.number, e.is_succeeded) for e in final_snapshot]))
|
||||
|
||||
BUILD_AGGREGATE_STATUS = 'BUILD_AGGREGATE_STATUS'
|
||||
others_snapshot = [el for el in final_snapshot if not el.is_leader]
|
||||
if reduce(lambda a, b: a and b, [e.is_succeeded for e in others_snapshot]):
|
||||
os.environ[BUILD_AGGREGATE_STATUS] = "others_succeeded"
|
||||
elif reduce(lambda a, b: a and b, [not e.is_succeeded for e in others_snapshot]):
|
||||
log.error("Others Failed")
|
||||
os.environ[BUILD_AGGREGATE_STATUS] = "others_failed"
|
||||
else:
|
||||
log.warn("Others Unknown")
|
||||
os.environ[BUILD_AGGREGATE_STATUS] = "unknown"
|
||||
#since python is subprocess, env variables are exported back via file
|
||||
with open(".to_export_back", "w") as export_var:
|
||||
export_var.write("BUILD_LEADER=YES {0}={1}".format(BUILD_AGGREGATE_STATUS, os.environ[BUILD_AGGREGATE_STATUS]))
|
||||
|
||||
except Exception as e:
|
||||
log.fatal(e)
|
||||
@@ -1,12 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
echo "[SCRIPT] Before Script :: Setup Parse DB for Postgres ${POSTGRES_MAJOR_VERSION}"
|
||||
|
||||
PGPASSWORD=postgres psql -v ON_ERROR_STOP=1 -h localhost -U postgres <<-EOSQL
|
||||
CREATE DATABASE parse_server_postgres_adapter_test_database;
|
||||
\c parse_server_postgres_adapter_test_database;
|
||||
CREATE EXTENSION postgis;
|
||||
CREATE EXTENSION postgis_topology;
|
||||
EOSQL
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"plugins": [
|
||||
"transform-object-rest-spread"
|
||||
],
|
||||
"presets": [
|
||||
["env", {
|
||||
"targets": {
|
||||
"node": "8"
|
||||
}
|
||||
}]
|
||||
],
|
||||
"sourceMaps": "inline",
|
||||
"retainLines": true
|
||||
}
|
||||
+4
-8
@@ -13,23 +13,19 @@
|
||||
"Item": true,
|
||||
"Container": true,
|
||||
"equal": true,
|
||||
"expectAsync": true,
|
||||
"notEqual": true,
|
||||
"it_only_db": true,
|
||||
"it_exclude_dbs": true,
|
||||
"describe_only_db": true,
|
||||
"describe_only": true,
|
||||
"on_db": true,
|
||||
"defaultConfiguration": true,
|
||||
"expectSuccess": true,
|
||||
"range": true,
|
||||
"expectError": true,
|
||||
"jequal": true,
|
||||
"create": true,
|
||||
"arrayContains": true,
|
||||
"expectAsync": true,
|
||||
"databaseAdapter": true
|
||||
"arrayContains": true
|
||||
},
|
||||
"rules": {
|
||||
"no-console": [0],
|
||||
"no-var": "error"
|
||||
"no-console": [0]
|
||||
}
|
||||
}
|
||||
|
||||
+209
-233
@@ -1,68 +1,64 @@
|
||||
'use strict';
|
||||
"use strict";
|
||||
|
||||
const Config = require('../lib/Config');
|
||||
const Config = require("../src/Config");
|
||||
|
||||
const loginWithWrongCredentialsShouldFail = function (username, password) {
|
||||
var loginWithWrongCredentialsShouldFail = function(username, password) {
|
||||
return new Promise((resolve, reject) => {
|
||||
Parse.User.logIn(username, password)
|
||||
.then(() => reject('login should have failed'))
|
||||
.catch(err => {
|
||||
if (err.message === 'Invalid username/password.') {
|
||||
resolve();
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var isAccountLockoutError = function(username, password, duration, waitTime) {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
Parse.User.logIn(username, password)
|
||||
.then(() => reject('login should have failed'))
|
||||
.catch(err => {
|
||||
if (err.message === 'Invalid username/password.') {
|
||||
if (err.message === 'Your account is locked due to multiple failed login attempts. Please try again after ' + duration + ' minute(s)') {
|
||||
resolve();
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const isAccountLockoutError = function (username, password, duration, waitTime) {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
Parse.User.logIn(username, password)
|
||||
.then(() => reject('login should have failed'))
|
||||
.catch(err => {
|
||||
if (
|
||||
err.message ===
|
||||
'Your account is locked due to multiple failed login attempts. Please try again after ' +
|
||||
duration +
|
||||
' minute(s)'
|
||||
) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}, waitTime);
|
||||
});
|
||||
};
|
||||
|
||||
describe('Account Lockout Policy: ', () => {
|
||||
describe("Account Lockout Policy: ", () => {
|
||||
|
||||
it('account should not be locked even after failed login attempts if account lockout policy is not set', done => {
|
||||
reconfigureServer({
|
||||
appName: 'unlimited',
|
||||
publicServerURL: 'http://localhost:1337/1',
|
||||
})
|
||||
.then(() => {
|
||||
const user = new Parse.User();
|
||||
user.setUsername('username1');
|
||||
user.setPassword('password');
|
||||
return user.signUp(null);
|
||||
})
|
||||
.then(() => {
|
||||
return loginWithWrongCredentialsShouldFail('username1', 'incorrect password 1');
|
||||
})
|
||||
.then(() => {
|
||||
return loginWithWrongCredentialsShouldFail('username1', 'incorrect password 2');
|
||||
})
|
||||
.then(() => {
|
||||
return loginWithWrongCredentialsShouldFail('username1', 'incorrect password 3');
|
||||
})
|
||||
.then(() => done())
|
||||
.catch(err => {
|
||||
fail('allow unlimited failed login attempts failed: ' + JSON.stringify(err));
|
||||
done();
|
||||
});
|
||||
.then(() => {
|
||||
var user = new Parse.User();
|
||||
user.setUsername('username1');
|
||||
user.setPassword('password');
|
||||
return user.signUp(null);
|
||||
})
|
||||
.then(() => {
|
||||
return loginWithWrongCredentialsShouldFail('username1', 'incorrect password 1');
|
||||
})
|
||||
.then(() => {
|
||||
return loginWithWrongCredentialsShouldFail('username1', 'incorrect password 2');
|
||||
})
|
||||
.then(() => {
|
||||
return loginWithWrongCredentialsShouldFail('username1', 'incorrect password 3');
|
||||
})
|
||||
.then(() => done())
|
||||
.catch(err => {
|
||||
fail('allow unlimited failed login attempts failed: ' + JSON.stringify(err));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('throw error if duration is set to an invalid number', done => {
|
||||
@@ -70,26 +66,23 @@ describe('Account Lockout Policy: ', () => {
|
||||
appName: 'duration',
|
||||
accountLockout: {
|
||||
duration: 'invalid value',
|
||||
threshold: 5,
|
||||
threshold: 5
|
||||
},
|
||||
publicServerURL: 'https://my.public.server.com/1',
|
||||
publicServerURL: "https://my.public.server.com/1"
|
||||
})
|
||||
.then(() => {
|
||||
Config.get('test');
|
||||
fail('set duration to an invalid number test failed');
|
||||
.then(() => {
|
||||
new Config('test');
|
||||
fail('set duration to an invalid number test failed');
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
if (err && err === 'Account lockout duration should be greater than 0 and less than 100000') {
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
if (
|
||||
err &&
|
||||
err === 'Account lockout duration should be greater than 0 and less than 100000'
|
||||
) {
|
||||
done();
|
||||
} else {
|
||||
fail('set duration to an invalid number test failed: ' + JSON.stringify(err));
|
||||
done();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
fail('set duration to an invalid number test failed: ' + JSON.stringify(err));
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('throw error if threshold is set to an invalid number', done => {
|
||||
@@ -97,26 +90,23 @@ describe('Account Lockout Policy: ', () => {
|
||||
appName: 'threshold',
|
||||
accountLockout: {
|
||||
duration: 5,
|
||||
threshold: 'invalid number',
|
||||
threshold: 'invalid number'
|
||||
},
|
||||
publicServerURL: 'https://my.public.server.com/1',
|
||||
publicServerURL: "https://my.public.server.com/1"
|
||||
})
|
||||
.then(() => {
|
||||
Config.get('test');
|
||||
fail('set threshold to an invalid number test failed');
|
||||
.then(() => {
|
||||
new Config('test');
|
||||
fail('set threshold to an invalid number test failed');
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
if (err && err === 'Account lockout threshold should be an integer greater than 0 and less than 1000') {
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
if (
|
||||
err &&
|
||||
err === 'Account lockout threshold should be an integer greater than 0 and less than 1000'
|
||||
) {
|
||||
done();
|
||||
} else {
|
||||
fail('set threshold to an invalid number test failed: ' + JSON.stringify(err));
|
||||
done();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
fail('set threshold to an invalid number test failed: ' + JSON.stringify(err));
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('throw error if threshold is < 1', done => {
|
||||
@@ -124,26 +114,23 @@ describe('Account Lockout Policy: ', () => {
|
||||
appName: 'threshold',
|
||||
accountLockout: {
|
||||
duration: 5,
|
||||
threshold: 0,
|
||||
threshold: 0
|
||||
},
|
||||
publicServerURL: 'https://my.public.server.com/1',
|
||||
publicServerURL: "https://my.public.server.com/1"
|
||||
})
|
||||
.then(() => {
|
||||
Config.get('test');
|
||||
fail('threshold value < 1 is invalid test failed');
|
||||
.then(() => {
|
||||
new Config('test');
|
||||
fail('threshold value < 1 is invalid test failed');
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
if (err && err === 'Account lockout threshold should be an integer greater than 0 and less than 1000') {
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
if (
|
||||
err &&
|
||||
err === 'Account lockout threshold should be an integer greater than 0 and less than 1000'
|
||||
) {
|
||||
done();
|
||||
} else {
|
||||
fail('threshold value < 1 is invalid test failed: ' + JSON.stringify(err));
|
||||
done();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
fail('threshold value < 1 is invalid test failed: ' + JSON.stringify(err));
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('throw error if threshold is > 999', done => {
|
||||
@@ -151,26 +138,23 @@ describe('Account Lockout Policy: ', () => {
|
||||
appName: 'threshold',
|
||||
accountLockout: {
|
||||
duration: 5,
|
||||
threshold: 1000,
|
||||
threshold: 1000
|
||||
},
|
||||
publicServerURL: 'https://my.public.server.com/1',
|
||||
publicServerURL: "https://my.public.server.com/1"
|
||||
})
|
||||
.then(() => {
|
||||
Config.get('test');
|
||||
fail('threshold value > 999 is invalid test failed');
|
||||
.then(() => {
|
||||
new Config('test');
|
||||
fail('threshold value > 999 is invalid test failed');
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
if (err && err === 'Account lockout threshold should be an integer greater than 0 and less than 1000') {
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
if (
|
||||
err &&
|
||||
err === 'Account lockout threshold should be an integer greater than 0 and less than 1000'
|
||||
) {
|
||||
done();
|
||||
} else {
|
||||
fail('threshold value > 999 is invalid test failed: ' + JSON.stringify(err));
|
||||
done();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
fail('threshold value > 999 is invalid test failed: ' + JSON.stringify(err));
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('throw error if duration is <= 0', done => {
|
||||
@@ -178,26 +162,23 @@ describe('Account Lockout Policy: ', () => {
|
||||
appName: 'duration',
|
||||
accountLockout: {
|
||||
duration: 0,
|
||||
threshold: 5,
|
||||
threshold: 5
|
||||
},
|
||||
publicServerURL: 'https://my.public.server.com/1',
|
||||
publicServerURL: "https://my.public.server.com/1"
|
||||
})
|
||||
.then(() => {
|
||||
Config.get('test');
|
||||
fail('duration value < 1 is invalid test failed');
|
||||
.then(() => {
|
||||
new Config('test');
|
||||
fail('duration value < 1 is invalid test failed');
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
if (err && err === 'Account lockout duration should be greater than 0 and less than 100000') {
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
if (
|
||||
err &&
|
||||
err === 'Account lockout duration should be greater than 0 and less than 100000'
|
||||
) {
|
||||
done();
|
||||
} else {
|
||||
fail('duration value < 1 is invalid test failed: ' + JSON.stringify(err));
|
||||
done();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
fail('duration value < 1 is invalid test failed: ' + JSON.stringify(err));
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('throw error if duration is > 99999', done => {
|
||||
@@ -205,26 +186,23 @@ describe('Account Lockout Policy: ', () => {
|
||||
appName: 'duration',
|
||||
accountLockout: {
|
||||
duration: 100000,
|
||||
threshold: 5,
|
||||
threshold: 5
|
||||
},
|
||||
publicServerURL: 'https://my.public.server.com/1',
|
||||
publicServerURL: "https://my.public.server.com/1"
|
||||
})
|
||||
.then(() => {
|
||||
Config.get('test');
|
||||
fail('duration value > 99999 is invalid test failed');
|
||||
.then(() => {
|
||||
new Config('test');
|
||||
fail('duration value > 99999 is invalid test failed');
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
if (err && err === 'Account lockout duration should be greater than 0 and less than 100000') {
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
if (
|
||||
err &&
|
||||
err === 'Account lockout duration should be greater than 0 and less than 100000'
|
||||
) {
|
||||
done();
|
||||
} else {
|
||||
fail('duration value > 99999 is invalid test failed: ' + JSON.stringify(err));
|
||||
done();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
fail('duration value > 99999 is invalid test failed: ' + JSON.stringify(err));
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('lock account if failed login attempts are above threshold', done => {
|
||||
@@ -232,32 +210,32 @@ describe('Account Lockout Policy: ', () => {
|
||||
appName: 'lockout threshold',
|
||||
accountLockout: {
|
||||
duration: 1,
|
||||
threshold: 2,
|
||||
threshold: 2
|
||||
},
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
})
|
||||
.then(() => {
|
||||
const user = new Parse.User();
|
||||
user.setUsername('username2');
|
||||
user.setPassword('failedLoginAttemptsThreshold');
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => {
|
||||
return loginWithWrongCredentialsShouldFail('username2', 'wrong password');
|
||||
})
|
||||
.then(() => {
|
||||
return loginWithWrongCredentialsShouldFail('username2', 'wrong password');
|
||||
})
|
||||
.then(() => {
|
||||
return isAccountLockoutError('username2', 'wrong password', 1, 1);
|
||||
})
|
||||
.then(() => {
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
fail('lock account after failed login attempts test failed: ' + JSON.stringify(err));
|
||||
done();
|
||||
});
|
||||
.then(() => {
|
||||
var user = new Parse.User();
|
||||
user.setUsername("username2");
|
||||
user.setPassword("failedLoginAttemptsThreshold");
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => {
|
||||
return loginWithWrongCredentialsShouldFail('username2', 'wrong password');
|
||||
})
|
||||
.then(() => {
|
||||
return loginWithWrongCredentialsShouldFail('username2', 'wrong password');
|
||||
})
|
||||
.then(() => {
|
||||
return isAccountLockoutError('username2', 'wrong password', 1, 1);
|
||||
})
|
||||
.then(() => {
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
fail('lock account after failed login attempts test failed: ' + JSON.stringify(err));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('lock account for accountPolicy.duration minutes if failed login attempts are above threshold', done => {
|
||||
@@ -265,36 +243,36 @@ describe('Account Lockout Policy: ', () => {
|
||||
appName: 'lockout threshold',
|
||||
accountLockout: {
|
||||
duration: 0.05, // 0.05*60 = 3 secs
|
||||
threshold: 2,
|
||||
threshold: 2
|
||||
},
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
})
|
||||
.then(() => {
|
||||
const user = new Parse.User();
|
||||
user.setUsername('username3');
|
||||
user.setPassword('failedLoginAttemptsThreshold');
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => {
|
||||
return loginWithWrongCredentialsShouldFail('username3', 'wrong password');
|
||||
})
|
||||
.then(() => {
|
||||
return loginWithWrongCredentialsShouldFail('username3', 'wrong password');
|
||||
})
|
||||
.then(() => {
|
||||
return isAccountLockoutError('username3', 'wrong password', 0.05, 1);
|
||||
})
|
||||
.then(() => {
|
||||
// account should still be locked even after 2 seconds.
|
||||
return isAccountLockoutError('username3', 'wrong password', 0.05, 2000);
|
||||
})
|
||||
.then(() => {
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
fail('account should be locked for duration mins test failed: ' + JSON.stringify(err));
|
||||
done();
|
||||
});
|
||||
.then(() => {
|
||||
var user = new Parse.User();
|
||||
user.setUsername("username3");
|
||||
user.setPassword("failedLoginAttemptsThreshold");
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => {
|
||||
return loginWithWrongCredentialsShouldFail('username3', 'wrong password');
|
||||
})
|
||||
.then(() => {
|
||||
return loginWithWrongCredentialsShouldFail('username3', 'wrong password');
|
||||
})
|
||||
.then(() => {
|
||||
return isAccountLockoutError('username3', 'wrong password', 0.05, 1);
|
||||
})
|
||||
.then(() => {
|
||||
// account should still be locked even after 2 seconds.
|
||||
return isAccountLockoutError('username3', 'wrong password', 0.05, 2000);
|
||||
})
|
||||
.then(() => {
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
fail('account should be locked for duration mins test failed: ' + JSON.stringify(err));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('allow login for locked account after accountPolicy.duration minutes', done => {
|
||||
@@ -302,41 +280,39 @@ describe('Account Lockout Policy: ', () => {
|
||||
appName: 'lockout threshold',
|
||||
accountLockout: {
|
||||
duration: 0.05, // 0.05*60 = 3 secs
|
||||
threshold: 2,
|
||||
threshold: 2
|
||||
},
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
})
|
||||
.then(() => {
|
||||
const user = new Parse.User();
|
||||
user.setUsername('username4');
|
||||
user.setPassword('correct password');
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => {
|
||||
return loginWithWrongCredentialsShouldFail('username4', 'wrong password');
|
||||
})
|
||||
.then(() => {
|
||||
return loginWithWrongCredentialsShouldFail('username4', 'wrong password');
|
||||
})
|
||||
.then(() => {
|
||||
// allow locked user to login after 3 seconds with a valid userid and password
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
Parse.User.logIn('username4', 'correct password')
|
||||
.then(() => resolve())
|
||||
.catch(err => reject(err));
|
||||
}, 3001);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
fail(
|
||||
'allow login for locked account after accountPolicy.duration minutes test failed: ' +
|
||||
JSON.stringify(err)
|
||||
);
|
||||
done();
|
||||
.then(() => {
|
||||
var user = new Parse.User();
|
||||
user.setUsername("username4");
|
||||
user.setPassword("correct password");
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => {
|
||||
return loginWithWrongCredentialsShouldFail('username4', 'wrong password');
|
||||
})
|
||||
.then(() => {
|
||||
return loginWithWrongCredentialsShouldFail('username4', 'wrong password');
|
||||
})
|
||||
.then(() => {
|
||||
// allow locked user to login after 3 seconds with a valid userid and password
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
Parse.User.logIn('username4', 'correct password')
|
||||
.then(() => resolve())
|
||||
.catch(err => reject(err));
|
||||
}, 3001);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
fail('allow login for locked account after accountPolicy.duration minutes test failed: ' + JSON.stringify(err));
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
@@ -1,84 +1,83 @@
|
||||
const AdaptableController = require('../lib/Controllers/AdaptableController').AdaptableController;
|
||||
const FilesAdapter = require('../lib/Adapters/Files/FilesAdapter').default;
|
||||
const FilesController = require('../lib/Controllers/FilesController').FilesController;
|
||||
|
||||
const MockController = function (options) {
|
||||
var AdaptableController = require("../src/Controllers/AdaptableController").AdaptableController;
|
||||
var FilesAdapter = require("../src/Adapters/Files/FilesAdapter").default;
|
||||
var FilesController = require("../src/Controllers/FilesController").FilesController;
|
||||
|
||||
var MockController = function(options) {
|
||||
AdaptableController.call(this, options);
|
||||
};
|
||||
}
|
||||
MockController.prototype = Object.create(AdaptableController.prototype);
|
||||
MockController.prototype.constructor = AdaptableController;
|
||||
|
||||
describe('AdaptableController', () => {
|
||||
it('should use the provided adapter', done => {
|
||||
const adapter = new FilesAdapter();
|
||||
const controller = new FilesController(adapter);
|
||||
describe("AdaptableController", ()=>{
|
||||
it("should use the provided adapter", (done) => {
|
||||
var adapter = new FilesAdapter();
|
||||
var controller = new FilesController(adapter);
|
||||
expect(controller.adapter).toBe(adapter);
|
||||
// make sure _adapter is private
|
||||
expect(controller._adapter).toBe(undefined);
|
||||
// Override _adapter is not doing anything
|
||||
controller._adapter = 'Hello';
|
||||
controller._adapter = "Hello";
|
||||
expect(controller.adapter).toBe(adapter);
|
||||
done();
|
||||
});
|
||||
|
||||
it('should throw when creating a new mock controller', done => {
|
||||
const adapter = new FilesAdapter();
|
||||
it("should throw when creating a new mock controller", (done) => {
|
||||
var adapter = new FilesAdapter();
|
||||
expect(() => {
|
||||
new MockController(adapter);
|
||||
}).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('should fail setting the wrong adapter to the controller', done => {
|
||||
it("should fail setting the wrong adapter to the controller", (done) => {
|
||||
function WrongAdapter() {}
|
||||
const adapter = new FilesAdapter();
|
||||
const controller = new FilesController(adapter);
|
||||
const otherAdapter = new WrongAdapter();
|
||||
var adapter = new FilesAdapter();
|
||||
var controller = new FilesController(adapter);
|
||||
var otherAdapter = new WrongAdapter();
|
||||
expect(() => {
|
||||
controller.adapter = otherAdapter;
|
||||
}).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('should fail to instantiate a controller with wrong adapter', done => {
|
||||
it("should fail to instantiate a controller with wrong adapter", (done) => {
|
||||
function WrongAdapter() {}
|
||||
const adapter = new WrongAdapter();
|
||||
var adapter = new WrongAdapter();
|
||||
expect(() => {
|
||||
new FilesController(adapter);
|
||||
}).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('should fail to instantiate a controller without an adapter', done => {
|
||||
it("should fail to instantiate a controller without an adapter", (done) => {
|
||||
expect(() => {
|
||||
new FilesController();
|
||||
}).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('should accept an object adapter', done => {
|
||||
const adapter = {
|
||||
createFile: function () {},
|
||||
deleteFile: function () {},
|
||||
getFileData: function () {},
|
||||
getFileLocation: function () {},
|
||||
validateFilename: function () {},
|
||||
};
|
||||
it("should accept an object adapter", (done) => {
|
||||
var adapter = {
|
||||
createFile: function() { },
|
||||
deleteFile: function() { },
|
||||
getFileData: function() { },
|
||||
getFileLocation: function() { },
|
||||
}
|
||||
expect(() => {
|
||||
new FilesController(adapter);
|
||||
}).not.toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('should accept an prototype based object adapter', done => {
|
||||
it("should accept an object adapter", (done) => {
|
||||
function AGoodAdapter() {}
|
||||
AGoodAdapter.prototype.createFile = function () {};
|
||||
AGoodAdapter.prototype.deleteFile = function () {};
|
||||
AGoodAdapter.prototype.getFileData = function () {};
|
||||
AGoodAdapter.prototype.getFileLocation = function () {};
|
||||
AGoodAdapter.prototype.validateFilename = function () {};
|
||||
AGoodAdapter.prototype.createFile = function() { };
|
||||
AGoodAdapter.prototype.deleteFile = function() { };
|
||||
AGoodAdapter.prototype.getFileData = function() { };
|
||||
AGoodAdapter.prototype.getFileLocation = function() { };
|
||||
|
||||
const adapter = new AGoodAdapter();
|
||||
var adapter = new AGoodAdapter();
|
||||
expect(() => {
|
||||
new FilesController(adapter);
|
||||
}).not.toThrow();
|
||||
|
||||
+56
-81
@@ -1,39 +1,40 @@
|
||||
const loadAdapter = require('../lib/Adapters/AdapterLoader').loadAdapter;
|
||||
const FilesAdapter = require('@parse/fs-files-adapter').default;
|
||||
const S3Adapter = require('@parse/s3-files-adapter').default;
|
||||
const ParsePushAdapter = require('@parse/push-adapter').default;
|
||||
const Config = require('../lib/Config');
|
||||
|
||||
describe('AdapterLoader', () => {
|
||||
it('should instantiate an adapter from string in object', done => {
|
||||
const adapterPath = require('path').resolve('./spec/MockAdapter');
|
||||
var loadAdapter = require("../src/Adapters/AdapterLoader").loadAdapter;
|
||||
var FilesAdapter = require("parse-server-fs-adapter").default;
|
||||
var S3Adapter = require("parse-server-s3-adapter").default;
|
||||
var ParsePushAdapter = require("parse-server-push-adapter").default;
|
||||
|
||||
const adapter = loadAdapter({
|
||||
describe("AdapterLoader", ()=>{
|
||||
|
||||
it("should instantiate an adapter from string in object", (done) => {
|
||||
var adapterPath = require('path').resolve("./spec/MockAdapter");
|
||||
|
||||
var adapter = loadAdapter({
|
||||
adapter: adapterPath,
|
||||
options: {
|
||||
key: 'value',
|
||||
foo: 'bar',
|
||||
},
|
||||
key: "value",
|
||||
foo: "bar"
|
||||
}
|
||||
});
|
||||
|
||||
expect(adapter instanceof Object).toBe(true);
|
||||
expect(adapter.options.key).toBe('value');
|
||||
expect(adapter.options.foo).toBe('bar');
|
||||
expect(adapter.options.key).toBe("value");
|
||||
expect(adapter.options.foo).toBe("bar");
|
||||
done();
|
||||
});
|
||||
|
||||
it('should instantiate an adapter from string', done => {
|
||||
const adapterPath = require('path').resolve('./spec/MockAdapter');
|
||||
const adapter = loadAdapter(adapterPath);
|
||||
it("should instantiate an adapter from string", (done) => {
|
||||
var adapterPath = require('path').resolve("./spec/MockAdapter");
|
||||
var adapter = loadAdapter(adapterPath);
|
||||
|
||||
expect(adapter instanceof Object).toBe(true);
|
||||
done();
|
||||
});
|
||||
|
||||
it('should instantiate an adapter from string that is module', done => {
|
||||
const adapterPath = require('path').resolve('./lib/Adapters/Files/FilesAdapter');
|
||||
const adapter = loadAdapter({
|
||||
adapter: adapterPath,
|
||||
it("should instantiate an adapter from string that is module", (done) => {
|
||||
var adapterPath = require('path').resolve("./src/Adapters/Files/FilesAdapter");
|
||||
var adapter = loadAdapter({
|
||||
adapter: adapterPath
|
||||
});
|
||||
|
||||
expect(typeof adapter).toBe('object');
|
||||
@@ -44,9 +45,9 @@ describe('AdapterLoader', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should instantiate an adapter from npm module', done => {
|
||||
const adapter = loadAdapter({
|
||||
module: '@parse/fs-files-adapter',
|
||||
it("should instantiate an adapter from npm module", (done) => {
|
||||
var adapter = loadAdapter({
|
||||
module: 'parse-server-fs-adapter'
|
||||
});
|
||||
|
||||
expect(typeof adapter).toBe('object');
|
||||
@@ -57,98 +58,72 @@ describe('AdapterLoader', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should instantiate an adapter from function/Class', done => {
|
||||
const adapter = loadAdapter({
|
||||
adapter: FilesAdapter,
|
||||
it("should instantiate an adapter from function/Class", (done) => {
|
||||
var adapter = loadAdapter({
|
||||
adapter: FilesAdapter
|
||||
});
|
||||
expect(adapter instanceof FilesAdapter).toBe(true);
|
||||
done();
|
||||
});
|
||||
|
||||
it('should instantiate the default adapter from Class', done => {
|
||||
const adapter = loadAdapter(null, FilesAdapter);
|
||||
it("should instantiate the default adapter from Class", (done) => {
|
||||
var adapter = loadAdapter(null, FilesAdapter);
|
||||
expect(adapter instanceof FilesAdapter).toBe(true);
|
||||
done();
|
||||
});
|
||||
|
||||
it('should use the default adapter', done => {
|
||||
const defaultAdapter = new FilesAdapter();
|
||||
const adapter = loadAdapter(null, defaultAdapter);
|
||||
it("should use the default adapter", (done) => {
|
||||
var defaultAdapter = new FilesAdapter();
|
||||
var adapter = loadAdapter(null, defaultAdapter);
|
||||
expect(adapter instanceof FilesAdapter).toBe(true);
|
||||
done();
|
||||
});
|
||||
|
||||
it('should use the provided adapter', done => {
|
||||
const originalAdapter = new FilesAdapter();
|
||||
const adapter = loadAdapter(originalAdapter);
|
||||
it("should use the provided adapter", (done) => {
|
||||
var originalAdapter = new FilesAdapter();
|
||||
var adapter = loadAdapter(originalAdapter);
|
||||
expect(adapter).toBe(originalAdapter);
|
||||
done();
|
||||
});
|
||||
|
||||
it('should fail loading an improperly configured adapter', done => {
|
||||
const Adapter = function (options) {
|
||||
it("should fail loading an improperly configured adapter", (done) => {
|
||||
var Adapter = function(options) {
|
||||
if (!options.foo) {
|
||||
throw 'foo is required for that adapter';
|
||||
throw "foo is required for that adapter";
|
||||
}
|
||||
};
|
||||
const adapterOptions = {
|
||||
param: 'key',
|
||||
doSomething: function () {},
|
||||
}
|
||||
var adapterOptions = {
|
||||
param: "key",
|
||||
doSomething: function() {}
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
const adapter = loadAdapter(adapterOptions, Adapter);
|
||||
var adapter = loadAdapter(adapterOptions, Adapter);
|
||||
expect(adapter).toEqual(adapterOptions);
|
||||
}).not.toThrow('foo is required for that adapter');
|
||||
}).not.toThrow("foo is required for that adapter");
|
||||
done();
|
||||
});
|
||||
|
||||
it('should load push adapter from options', done => {
|
||||
const options = {
|
||||
android: {
|
||||
senderId: 'yolo',
|
||||
apiKey: 'yolo',
|
||||
},
|
||||
};
|
||||
it("should load push adapter from options", (done) => {
|
||||
var options = {
|
||||
ios: {
|
||||
bundleId: 'bundle.id'
|
||||
}
|
||||
}
|
||||
expect(() => {
|
||||
const adapter = loadAdapter(undefined, ParsePushAdapter, options);
|
||||
var 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 => {
|
||||
const adapterPath = require('path').resolve('./spec/MockPushAdapter');
|
||||
const options = {
|
||||
ios: {
|
||||
bundleId: 'bundle.id',
|
||||
},
|
||||
};
|
||||
const pushAdapterOptions = {
|
||||
adapter: adapterPath,
|
||||
options,
|
||||
};
|
||||
it("should load S3Adapter from direct passing", (done) => {
|
||||
var s3Adapter = new S3Adapter("key", "secret", "bucket")
|
||||
expect(() => {
|
||||
reconfigureServer({
|
||||
push: pushAdapterOptions,
|
||||
}).then(() => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const pushAdapter = config.pushWorker.adapter;
|
||||
expect(pushAdapter.getValidPushTypes()).toEqual(['ios']);
|
||||
expect(pushAdapter.options).toEqual(pushAdapterOptions);
|
||||
done();
|
||||
});
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should load S3Adapter from direct passing', done => {
|
||||
spyOn(console, 'warn').and.callFake(() => {});
|
||||
const s3Adapter = new S3Adapter('key', 'secret', 'bucket');
|
||||
expect(() => {
|
||||
const adapter = loadAdapter(s3Adapter, FilesAdapter);
|
||||
var adapter = loadAdapter(s3Adapter, FilesAdapter);
|
||||
expect(adapter).toBe(s3Adapter);
|
||||
}).not.toThrow();
|
||||
done();
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
const AggregateRouter = require('../lib/Routers/AggregateRouter').AggregateRouter;
|
||||
|
||||
describe('AggregateRouter', () => {
|
||||
it('get pipeline from Array', () => {
|
||||
const body = [
|
||||
{
|
||||
group: { objectId: {} },
|
||||
},
|
||||
];
|
||||
const expected = [{ $group: { _id: {} } }];
|
||||
const result = AggregateRouter.getPipeline(body);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('get pipeline from Object', () => {
|
||||
const body = {
|
||||
group: { objectId: {} },
|
||||
};
|
||||
const expected = [{ $group: { _id: {} } }];
|
||||
const result = AggregateRouter.getPipeline(body);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('get pipeline from Pipeline Operator (Array)', () => {
|
||||
const body = {
|
||||
pipeline: [
|
||||
{
|
||||
group: { objectId: {} },
|
||||
},
|
||||
],
|
||||
};
|
||||
const expected = [{ $group: { _id: {} } }];
|
||||
const result = AggregateRouter.getPipeline(body);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('get pipeline from Pipeline Operator (Object)', () => {
|
||||
const body = {
|
||||
pipeline: {
|
||||
group: { objectId: {} },
|
||||
},
|
||||
};
|
||||
const expected = [{ $group: { _id: {} } }];
|
||||
const result = AggregateRouter.getPipeline(body);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('get pipeline fails multiple keys in Array stage ', () => {
|
||||
const body = [
|
||||
{
|
||||
group: { objectId: {} },
|
||||
match: { name: 'Test' },
|
||||
},
|
||||
];
|
||||
try {
|
||||
AggregateRouter.getPipeline(body);
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('Pipeline stages should only have one key found group, match');
|
||||
}
|
||||
});
|
||||
|
||||
it('get pipeline fails multiple keys in Pipeline Operator Array stage ', () => {
|
||||
const body = {
|
||||
pipeline: [
|
||||
{
|
||||
group: { objectId: {} },
|
||||
match: { name: 'Test' },
|
||||
},
|
||||
],
|
||||
};
|
||||
try {
|
||||
AggregateRouter.getPipeline(body);
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('Pipeline stages should only have one key found group, match');
|
||||
}
|
||||
});
|
||||
});
|
||||
+48
-56
@@ -1,69 +1,61 @@
|
||||
const analyticsAdapter = {
|
||||
appOpened: function () {},
|
||||
trackEvent: function () {},
|
||||
};
|
||||
appOpened: function() {},
|
||||
trackEvent: function() {}
|
||||
}
|
||||
|
||||
describe('AnalyticsController', () => {
|
||||
it('should track a simple event', done => {
|
||||
it('should track a simple event', (done) => {
|
||||
|
||||
spyOn(analyticsAdapter, 'trackEvent').and.callThrough();
|
||||
reconfigureServer({
|
||||
analyticsAdapter,
|
||||
})
|
||||
.then(() => {
|
||||
return Parse.Analytics.track('MyEvent', {
|
||||
key: 'value',
|
||||
count: '0',
|
||||
});
|
||||
analyticsAdapter
|
||||
}).then(() => {
|
||||
return Parse.Analytics.track('MyEvent', {
|
||||
key: 'value',
|
||||
count: '0'
|
||||
})
|
||||
.then(
|
||||
() => {
|
||||
expect(analyticsAdapter.trackEvent).toHaveBeenCalled();
|
||||
const lastCall = analyticsAdapter.trackEvent.calls.first();
|
||||
const args = lastCall.args;
|
||||
expect(args[0]).toEqual('MyEvent');
|
||||
expect(args[1]).toEqual({
|
||||
dimensions: {
|
||||
key: 'value',
|
||||
count: '0',
|
||||
},
|
||||
});
|
||||
done();
|
||||
},
|
||||
err => {
|
||||
fail(JSON.stringify(err));
|
||||
done();
|
||||
}).then(() => {
|
||||
expect(analyticsAdapter.trackEvent).toHaveBeenCalled();
|
||||
var lastCall = analyticsAdapter.trackEvent.calls.first();
|
||||
const args = lastCall.args;
|
||||
expect(args[0]).toEqual('MyEvent');
|
||||
expect(args[1]).toEqual({
|
||||
dimensions: {
|
||||
key: 'value',
|
||||
count: '0'
|
||||
}
|
||||
);
|
||||
});
|
||||
done();
|
||||
}, (err) => {
|
||||
fail(JSON.stringify(err));
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('should track a app opened event', done => {
|
||||
it('should track a app opened event', (done) => {
|
||||
|
||||
spyOn(analyticsAdapter, 'appOpened').and.callThrough();
|
||||
reconfigureServer({
|
||||
analyticsAdapter,
|
||||
})
|
||||
.then(() => {
|
||||
return Parse.Analytics.track('AppOpened', {
|
||||
key: 'value',
|
||||
count: '0',
|
||||
});
|
||||
analyticsAdapter
|
||||
}).then(() => {
|
||||
return Parse.Analytics.track('AppOpened', {
|
||||
key: 'value',
|
||||
count: '0'
|
||||
})
|
||||
.then(
|
||||
() => {
|
||||
expect(analyticsAdapter.appOpened).toHaveBeenCalled();
|
||||
const lastCall = analyticsAdapter.appOpened.calls.first();
|
||||
const args = lastCall.args;
|
||||
expect(args[0]).toEqual({
|
||||
dimensions: {
|
||||
key: 'value',
|
||||
count: '0',
|
||||
},
|
||||
});
|
||||
done();
|
||||
},
|
||||
err => {
|
||||
fail(JSON.stringify(err));
|
||||
done();
|
||||
}).then(() => {
|
||||
expect(analyticsAdapter.appOpened).toHaveBeenCalled();
|
||||
var lastCall = analyticsAdapter.appOpened.calls.first();
|
||||
const args = lastCall.args;
|
||||
expect(args[0]).toEqual({
|
||||
dimensions: {
|
||||
key: 'value',
|
||||
count: '0'
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
done();
|
||||
}, (err) => {
|
||||
fail(JSON.stringify(err));
|
||||
done();
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,421 +0,0 @@
|
||||
const auth = require('../lib/Auth');
|
||||
const Config = require('../lib/Config');
|
||||
const rest = require('../lib/rest');
|
||||
const request = require('../lib/request');
|
||||
const AudiencesRouter = require('../lib/Routers/AudiencesRouter').AudiencesRouter;
|
||||
|
||||
describe('AudiencesRouter', () => {
|
||||
it('uses find condition from request.body', done => {
|
||||
const config = Config.get('test');
|
||||
const androidAudienceRequest = {
|
||||
name: 'Android Users',
|
||||
query: '{ "test": "android" }',
|
||||
};
|
||||
const iosAudienceRequest = {
|
||||
name: 'Iphone Users',
|
||||
query: '{ "test": "ios" }',
|
||||
};
|
||||
const request = {
|
||||
config: config,
|
||||
auth: auth.master(config),
|
||||
body: {
|
||||
where: {
|
||||
query: '{ "test": "android" }',
|
||||
},
|
||||
},
|
||||
query: {},
|
||||
info: {},
|
||||
};
|
||||
|
||||
const router = new AudiencesRouter();
|
||||
rest
|
||||
.create(config, auth.nobody(config), '_Audience', androidAudienceRequest)
|
||||
.then(() => {
|
||||
return rest.create(config, auth.nobody(config), '_Audience', iosAudienceRequest);
|
||||
})
|
||||
.then(() => {
|
||||
return router.handleFind(request);
|
||||
})
|
||||
.then(res => {
|
||||
const results = res.response.results;
|
||||
expect(results.length).toEqual(1);
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
fail(JSON.stringify(err));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('uses find condition from request.query', done => {
|
||||
const config = Config.get('test');
|
||||
const androidAudienceRequest = {
|
||||
name: 'Android Users',
|
||||
query: '{ "test": "android" }',
|
||||
};
|
||||
const iosAudienceRequest = {
|
||||
name: 'Iphone Users',
|
||||
query: '{ "test": "ios" }',
|
||||
};
|
||||
const request = {
|
||||
config: config,
|
||||
auth: auth.master(config),
|
||||
body: {},
|
||||
query: {
|
||||
where: {
|
||||
query: '{ "test": "android" }',
|
||||
},
|
||||
},
|
||||
info: {},
|
||||
};
|
||||
|
||||
const router = new AudiencesRouter();
|
||||
rest
|
||||
.create(config, auth.nobody(config), '_Audience', androidAudienceRequest)
|
||||
.then(() => {
|
||||
return rest.create(config, auth.nobody(config), '_Audience', iosAudienceRequest);
|
||||
})
|
||||
.then(() => {
|
||||
return router.handleFind(request);
|
||||
})
|
||||
.then(res => {
|
||||
const results = res.response.results;
|
||||
expect(results.length).toEqual(1);
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
fail(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('query installations with limit = 0', done => {
|
||||
const config = Config.get('test');
|
||||
const androidAudienceRequest = {
|
||||
name: 'Android Users',
|
||||
query: '{ "test": "android" }',
|
||||
};
|
||||
const iosAudienceRequest = {
|
||||
name: 'Iphone Users',
|
||||
query: '{ "test": "ios" }',
|
||||
};
|
||||
const request = {
|
||||
config: config,
|
||||
auth: auth.master(config),
|
||||
body: {},
|
||||
query: {
|
||||
limit: 0,
|
||||
},
|
||||
info: {},
|
||||
};
|
||||
|
||||
Config.get('test');
|
||||
const router = new AudiencesRouter();
|
||||
rest
|
||||
.create(config, auth.nobody(config), '_Audience', androidAudienceRequest)
|
||||
.then(() => {
|
||||
return rest.create(config, auth.nobody(config), '_Audience', iosAudienceRequest);
|
||||
})
|
||||
.then(() => {
|
||||
return router.handleFind(request);
|
||||
})
|
||||
.then(res => {
|
||||
const response = res.response;
|
||||
expect(response.results.length).toEqual(0);
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
fail(JSON.stringify(err));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('query installations with count = 1', done => {
|
||||
const config = Config.get('test');
|
||||
const androidAudienceRequest = {
|
||||
name: 'Android Users',
|
||||
query: '{ "test": "android" }',
|
||||
};
|
||||
const iosAudienceRequest = {
|
||||
name: 'Iphone Users',
|
||||
query: '{ "test": "ios" }',
|
||||
};
|
||||
const request = {
|
||||
config: config,
|
||||
auth: auth.master(config),
|
||||
body: {},
|
||||
query: {
|
||||
count: 1,
|
||||
},
|
||||
info: {},
|
||||
};
|
||||
|
||||
const router = new AudiencesRouter();
|
||||
rest
|
||||
.create(config, auth.nobody(config), '_Audience', androidAudienceRequest)
|
||||
.then(() => rest.create(config, auth.nobody(config), '_Audience', iosAudienceRequest))
|
||||
.then(() => router.handleFind(request))
|
||||
.then(res => {
|
||||
const response = res.response;
|
||||
expect(response.results.length).toEqual(2);
|
||||
expect(response.count).toEqual(2);
|
||||
done();
|
||||
})
|
||||
.catch(error => {
|
||||
fail(JSON.stringify(error));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('query installations with limit = 0 and count = 1', done => {
|
||||
const config = Config.get('test');
|
||||
const androidAudienceRequest = {
|
||||
name: 'Android Users',
|
||||
query: '{ "test": "android" }',
|
||||
};
|
||||
const iosAudienceRequest = {
|
||||
name: 'Iphone Users',
|
||||
query: '{ "test": "ios" }',
|
||||
};
|
||||
const request = {
|
||||
config: config,
|
||||
auth: auth.master(config),
|
||||
body: {},
|
||||
query: {
|
||||
limit: 0,
|
||||
count: 1,
|
||||
},
|
||||
info: {},
|
||||
};
|
||||
|
||||
const router = new AudiencesRouter();
|
||||
rest
|
||||
.create(config, auth.nobody(config), '_Audience', androidAudienceRequest)
|
||||
.then(() => {
|
||||
return rest.create(config, auth.nobody(config), '_Audience', iosAudienceRequest);
|
||||
})
|
||||
.then(() => {
|
||||
return router.handleFind(request);
|
||||
})
|
||||
.then(res => {
|
||||
const response = res.response;
|
||||
expect(response.results.length).toEqual(0);
|
||||
expect(response.count).toEqual(2);
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
fail(JSON.stringify(err));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should create, read, update and delete audiences throw api', done => {
|
||||
Parse._request(
|
||||
'POST',
|
||||
'push_audiences',
|
||||
{ name: 'My Audience', query: JSON.stringify({ deviceType: 'ios' }) },
|
||||
{ useMasterKey: true }
|
||||
).then(() => {
|
||||
Parse._request('GET', 'push_audiences', {}, { useMasterKey: true }).then(results => {
|
||||
expect(results.results.length).toEqual(1);
|
||||
expect(results.results[0].name).toEqual('My Audience');
|
||||
expect(results.results[0].query.deviceType).toEqual('ios');
|
||||
Parse._request(
|
||||
'GET',
|
||||
`push_audiences/${results.results[0].objectId}`,
|
||||
{},
|
||||
{ useMasterKey: true }
|
||||
).then(results => {
|
||||
expect(results.name).toEqual('My Audience');
|
||||
expect(results.query.deviceType).toEqual('ios');
|
||||
Parse._request(
|
||||
'PUT',
|
||||
`push_audiences/${results.objectId}`,
|
||||
{ name: 'My Audience 2' },
|
||||
{ useMasterKey: true }
|
||||
).then(() => {
|
||||
Parse._request(
|
||||
'GET',
|
||||
`push_audiences/${results.objectId}`,
|
||||
{},
|
||||
{ useMasterKey: true }
|
||||
).then(results => {
|
||||
expect(results.name).toEqual('My Audience 2');
|
||||
expect(results.query.deviceType).toEqual('ios');
|
||||
Parse._request(
|
||||
'DELETE',
|
||||
`push_audiences/${results.objectId}`,
|
||||
{},
|
||||
{ useMasterKey: true }
|
||||
).then(() => {
|
||||
Parse._request('GET', 'push_audiences', {}, { useMasterKey: true }).then(
|
||||
results => {
|
||||
expect(results.results.length).toEqual(0);
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should only create with master key', done => {
|
||||
Parse._request('POST', 'push_audiences', {
|
||||
name: 'My Audience',
|
||||
query: JSON.stringify({ deviceType: 'ios' }),
|
||||
}).then(
|
||||
() => {},
|
||||
error => {
|
||||
expect(error.message).toEqual('unauthorized: master key is required');
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should only find with master key', done => {
|
||||
Parse._request('GET', 'push_audiences', {}).then(
|
||||
() => {},
|
||||
error => {
|
||||
expect(error.message).toEqual('unauthorized: master key is required');
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should only get with master key', done => {
|
||||
Parse._request('GET', `push_audiences/someId`, {}).then(
|
||||
() => {},
|
||||
error => {
|
||||
expect(error.message).toEqual('unauthorized: master key is required');
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should only update with master key', done => {
|
||||
Parse._request('PUT', `push_audiences/someId`, {
|
||||
name: 'My Audience 2',
|
||||
}).then(
|
||||
() => {},
|
||||
error => {
|
||||
expect(error.message).toEqual('unauthorized: master key is required');
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should only delete with master key', done => {
|
||||
Parse._request('DELETE', `push_audiences/someId`, {}).then(
|
||||
() => {},
|
||||
error => {
|
||||
expect(error.message).toEqual('unauthorized: master key is required');
|
||||
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(
|
||||
'POST',
|
||||
'push_audiences',
|
||||
{ name: 'My Audience', query: JSON.stringify({ deviceType: 'ios' }) },
|
||||
{ useMasterKey: true }
|
||||
).then(audience => {
|
||||
database.collection('test__Audience').updateOne(
|
||||
{ _id: audience.objectId },
|
||||
{
|
||||
$set: {
|
||||
times_used: 1,
|
||||
_last_used: now,
|
||||
},
|
||||
},
|
||||
{},
|
||||
error => {
|
||||
expect(error).toEqual(null);
|
||||
database
|
||||
.collection('test__Audience')
|
||||
.find({ _id: audience.objectId })
|
||||
.toArray((error, rows) => {
|
||||
expect(error).toEqual(null);
|
||||
expect(rows[0]['times_used']).toEqual(1);
|
||||
expect(rows[0]['_last_used']).toEqual(now);
|
||||
Parse._request(
|
||||
'GET',
|
||||
'push_audiences/' + audience.objectId,
|
||||
{},
|
||||
{ useMasterKey: true }
|
||||
)
|
||||
.then(audience => {
|
||||
expect(audience.name).toEqual('My Audience');
|
||||
expect(audience.query.deviceType).toEqual('ios');
|
||||
expect(audience.timesUsed).toEqual(1);
|
||||
expect(audience.lastUsed).toEqual(now.toISOString());
|
||||
done();
|
||||
})
|
||||
.catch(error => {
|
||||
done.fail(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to search on audiences', done => {
|
||||
Parse._request(
|
||||
'POST',
|
||||
'push_audiences',
|
||||
{ name: 'neverUsed', query: JSON.stringify({ deviceType: 'ios' }) },
|
||||
{ useMasterKey: true }
|
||||
).then(() => {
|
||||
const query = {
|
||||
timesUsed: { $exists: false },
|
||||
lastUsed: { $exists: false },
|
||||
};
|
||||
Parse._request(
|
||||
'GET',
|
||||
'push_audiences?order=-createdAt&limit=1',
|
||||
{ where: query },
|
||||
{ useMasterKey: true }
|
||||
)
|
||||
.then(results => {
|
||||
expect(results.results.length).toEqual(1);
|
||||
const audience = results.results[0];
|
||||
expect(audience.name).toEqual('neverUsed');
|
||||
done();
|
||||
})
|
||||
.catch(error => {
|
||||
done.fail(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle _Audience invalid fields via rest', async () => {
|
||||
await reconfigureServer({
|
||||
appId: 'test',
|
||||
restAPIKey: 'test',
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
});
|
||||
try {
|
||||
await request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/classes/_Audience',
|
||||
body: { lorem: 'ipsum', _method: 'POST' },
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'test',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
expect(true).toBeFalsy();
|
||||
} catch (e) {
|
||||
expect(e.data.code).toBe(107);
|
||||
expect(e.data.error).toBe('Could not add field lorem');
|
||||
}
|
||||
});
|
||||
});
|
||||
+56
-206
@@ -1,11 +1,11 @@
|
||||
describe('Auth', () => {
|
||||
const { Auth, getAuthForSessionToken } = require('../lib/Auth.js');
|
||||
const Config = require('../lib/Config');
|
||||
var Auth = require('../src/Auth.js').Auth;
|
||||
|
||||
describe('getUserRoles', () => {
|
||||
let auth;
|
||||
let config;
|
||||
let currentRoles = null;
|
||||
const currentUserId = 'userId';
|
||||
var auth;
|
||||
var config;
|
||||
var currentRoles = null;
|
||||
var currentUserId = 'userId';
|
||||
|
||||
beforeEach(() => {
|
||||
currentRoles = ['role:userId'];
|
||||
@@ -14,230 +14,80 @@ describe('Auth', () => {
|
||||
cacheController: {
|
||||
role: {
|
||||
get: () => Promise.resolve(currentRoles),
|
||||
set: jasmine.createSpy('set'),
|
||||
},
|
||||
},
|
||||
};
|
||||
set: jasmine.createSpy('set')
|
||||
}
|
||||
}
|
||||
}
|
||||
spyOn(config.cacheController.role, 'get').and.callThrough();
|
||||
|
||||
auth = new Auth({
|
||||
config: config,
|
||||
isMaster: false,
|
||||
user: {
|
||||
id: currentUserId,
|
||||
id: currentUserId
|
||||
},
|
||||
installationId: 'installationId',
|
||||
installationId: 'installationId'
|
||||
});
|
||||
});
|
||||
|
||||
it('should get user roles from the cache', done => {
|
||||
auth.getUserRoles().then(roles => {
|
||||
const firstSet = config.cacheController.role.set.calls.first();
|
||||
expect(firstSet).toEqual(undefined);
|
||||
it('should get user roles from the cache', (done) => {
|
||||
auth.getUserRoles()
|
||||
.then((roles) => {
|
||||
var firstSet = config.cacheController.role.set.calls.first();
|
||||
expect(firstSet).toEqual(undefined);
|
||||
|
||||
const firstGet = config.cacheController.role.get.calls.first();
|
||||
expect(firstGet.args[0]).toEqual(currentUserId);
|
||||
expect(roles).toEqual(currentRoles);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should only query the roles once', done => {
|
||||
const loadRolesSpy = spyOn(auth, '_loadRoles').and.callThrough();
|
||||
auth
|
||||
.getUserRoles()
|
||||
.then(roles => {
|
||||
expect(roles).toEqual(currentRoles);
|
||||
return auth.getUserRoles();
|
||||
})
|
||||
.then(() => auth.getUserRoles())
|
||||
.then(() => auth.getUserRoles())
|
||||
.then(roles => {
|
||||
// Should only call the cache adapter once.
|
||||
expect(config.cacheController.role.get.calls.count()).toEqual(1);
|
||||
expect(loadRolesSpy.calls.count()).toEqual(1);
|
||||
|
||||
const firstGet = config.cacheController.role.get.calls.first();
|
||||
var firstGet = config.cacheController.role.get.calls.first();
|
||||
expect(firstGet.args[0]).toEqual(currentUserId);
|
||||
expect(roles).toEqual(currentRoles);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not have any roles with no user', done => {
|
||||
auth.user = null;
|
||||
auth
|
||||
.getUserRoles()
|
||||
.then(roles => expect(roles).toEqual([]))
|
||||
.then(() => done());
|
||||
});
|
||||
it('should only query the roles once', (done) => {
|
||||
var loadRolesSpy = spyOn(auth, '_loadRoles').and.callThrough();
|
||||
auth.getUserRoles()
|
||||
.then((roles) => {
|
||||
expect(roles).toEqual(currentRoles);
|
||||
return auth.getUserRoles()
|
||||
})
|
||||
.then(() => auth.getUserRoles())
|
||||
.then(() => auth.getUserRoles())
|
||||
.then((roles) => {
|
||||
// Should only call the cache adapter once.
|
||||
expect(config.cacheController.role.get.calls.count()).toEqual(1);
|
||||
expect(loadRolesSpy.calls.count()).toEqual(1);
|
||||
|
||||
it('should not have any user roles with master', done => {
|
||||
auth.isMaster = true;
|
||||
auth
|
||||
.getUserRoles()
|
||||
.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();
|
||||
var firstGet = config.cacheController.role.get.calls.first();
|
||||
expect(firstGet.args[0]).toEqual(currentUserId);
|
||||
expect(roles).toEqual(currentRoles);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should load auth without 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(),
|
||||
});
|
||||
expect(userAuth.user instanceof Parse.User).toBe(true);
|
||||
expect(userAuth.user.id).toBe(user.id);
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const user = new Parse.User();
|
||||
await user.signUp({
|
||||
username: 'hello',
|
||||
password: 'password',
|
||||
});
|
||||
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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
describe('getRolesForUser', () => {
|
||||
const rolesNumber = 300;
|
||||
|
||||
it('should load all roles without 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(),
|
||||
});
|
||||
const roles = [];
|
||||
for (let i = 0; i < rolesNumber; i++) {
|
||||
const acl = new Parse.ACL();
|
||||
const role = new Parse.Role('roleloadtest' + i, acl);
|
||||
role.getUsers().add([user]);
|
||||
roles.push(role.save());
|
||||
}
|
||||
const savedRoles = await Promise.all(roles);
|
||||
expect(savedRoles.length).toBe(rolesNumber);
|
||||
const cloudRoles = await userAuth.getRolesForUser();
|
||||
expect(cloudRoles.length).toBe(rolesNumber);
|
||||
});
|
||||
|
||||
it('should load all roles with 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'),
|
||||
});
|
||||
const roles = [];
|
||||
for (let i = 0; i < rolesNumber; i++) {
|
||||
const acl = new Parse.ACL();
|
||||
const role = new Parse.Role('roleloadtest' + i, acl);
|
||||
role.getUsers().add([user]);
|
||||
roles.push(role.save());
|
||||
}
|
||||
const savedRoles = await Promise.all(roles);
|
||||
expect(savedRoles.length).toBe(rolesNumber);
|
||||
const cloudRoles = await userAuth.getRolesForUser();
|
||||
expect(cloudRoles.length).toBe(rolesNumber);
|
||||
it('should not have any roles with no user', (done) => {
|
||||
auth.user = null
|
||||
auth.getUserRoles()
|
||||
.then((roles) => expect(roles).toEqual([]))
|
||||
.then(() => done());
|
||||
});
|
||||
|
||||
it('should load all roles for different users with config', async () => {
|
||||
const rolesNumber = 100;
|
||||
const user = new Parse.User();
|
||||
await user.signUp({
|
||||
username: 'hello',
|
||||
password: 'password',
|
||||
});
|
||||
const user2 = new Parse.User();
|
||||
await user2.signUp({
|
||||
username: 'world',
|
||||
password: '1234',
|
||||
});
|
||||
expect(user.getSessionToken()).not.toBeUndefined();
|
||||
const userAuth = await getAuthForSessionToken({
|
||||
sessionToken: user.getSessionToken(),
|
||||
config: Config.get('test'),
|
||||
});
|
||||
const user2Auth = await getAuthForSessionToken({
|
||||
sessionToken: user2.getSessionToken(),
|
||||
config: Config.get('test'),
|
||||
});
|
||||
const roles = [];
|
||||
for (let i = 0; i < rolesNumber; i += 1) {
|
||||
const acl = new Parse.ACL();
|
||||
const acl2 = new Parse.ACL();
|
||||
const role = new Parse.Role('roleloadtest' + i, acl);
|
||||
const role2 = new Parse.Role('role2loadtest' + i, acl2);
|
||||
role.getUsers().add([user]);
|
||||
role2.getUsers().add([user2]);
|
||||
roles.push(role.save());
|
||||
roles.push(role2.save());
|
||||
}
|
||||
const savedRoles = await Promise.all(roles);
|
||||
expect(savedRoles.length).toBe(rolesNumber * 2);
|
||||
const cloudRoles = await userAuth.getRolesForUser();
|
||||
const cloudRoles2 = await user2Auth.getRolesForUser();
|
||||
expect(cloudRoles.length).toBe(rolesNumber);
|
||||
expect(cloudRoles2.length).toBe(rolesNumber);
|
||||
it('should not have any user roles with master', (done) => {
|
||||
auth.isMaster = true
|
||||
auth.getUserRoles()
|
||||
.then((roles) => expect(roles).toEqual([]))
|
||||
.then(() => done());
|
||||
});
|
||||
|
||||
it('should properly handle bcrypt upgrade', (done) => {
|
||||
var bcryptOriginal = require('bcrypt-nodejs');
|
||||
var bcryptNew = require('bcryptjs');
|
||||
bcryptOriginal.hash('my1Long:password', null, null, function(err, res) {
|
||||
bcryptNew.compare('my1Long:password', res, function(err, res) {
|
||||
expect(res).toBeTruthy();
|
||||
done();
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
+169
-1769
File diff suppressed because it is too large
Load Diff
+49
-157
@@ -1,34 +1,32 @@
|
||||
'use strict';
|
||||
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');
|
||||
const { spawn } = require('child_process');
|
||||
import commander from '../src/cli/utils/commander';
|
||||
import definitions from '../src/cli/definitions/parse-server';
|
||||
import liveQueryDefinitions from '../src/cli/definitions/parse-live-query-server';
|
||||
|
||||
const testDefinitions = {
|
||||
arg0: 'PROGRAM_ARG_0',
|
||||
arg1: {
|
||||
var testDefinitions = {
|
||||
'arg0': 'PROGRAM_ARG_0',
|
||||
'arg1': {
|
||||
env: 'PROGRAM_ARG_1',
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
arg2: {
|
||||
'arg2': {
|
||||
env: 'PROGRAM_ARG_2',
|
||||
action: function (value) {
|
||||
const intValue = parseInt(value);
|
||||
action: function(value) {
|
||||
var intValue = parseInt(value);
|
||||
if (!Number.isInteger(intValue)) {
|
||||
throw 'arg2 is invalid';
|
||||
}
|
||||
return intValue;
|
||||
},
|
||||
},
|
||||
arg3: {},
|
||||
arg4: {
|
||||
default: 'arg4Value',
|
||||
}
|
||||
},
|
||||
'arg3': {},
|
||||
'arg4': {
|
||||
default: 'arg4Value'
|
||||
}
|
||||
};
|
||||
|
||||
describe('commander additions', () => {
|
||||
afterEach(done => {
|
||||
afterEach((done) => {
|
||||
commander.options = [];
|
||||
delete commander.arg0;
|
||||
delete commander.arg1;
|
||||
@@ -38,20 +36,9 @@ describe('commander additions', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should load properly definitions from args', done => {
|
||||
it('should load properly definitions from args', (done) => {
|
||||
commander.loadDefinitions(testDefinitions);
|
||||
commander.parse([
|
||||
'node',
|
||||
'./CLI.spec.js',
|
||||
'--arg0',
|
||||
'arg0Value',
|
||||
'--arg1',
|
||||
'arg1Value',
|
||||
'--arg2',
|
||||
'2',
|
||||
'--arg3',
|
||||
'some',
|
||||
]);
|
||||
commander.parse(['node','./CLI.spec.js','--arg0', 'arg0Value', '--arg1', 'arg1Value', '--arg2', '2', '--arg3', 'some']);
|
||||
expect(commander.arg0).toEqual('arg0Value');
|
||||
expect(commander.arg1).toEqual('arg1Value');
|
||||
expect(commander.arg2).toEqual(2);
|
||||
@@ -60,12 +47,12 @@ describe('commander additions', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should load properly definitions from env', done => {
|
||||
it('should load properly definitions from env', (done) => {
|
||||
commander.loadDefinitions(testDefinitions);
|
||||
commander.parse([], {
|
||||
PROGRAM_ARG_0: 'arg0ENVValue',
|
||||
PROGRAM_ARG_1: 'arg1ENVValue',
|
||||
PROGRAM_ARG_2: '3',
|
||||
'PROGRAM_ARG_0': 'arg0ENVValue',
|
||||
'PROGRAM_ARG_1': 'arg1ENVValue',
|
||||
'PROGRAM_ARG_2': '3',
|
||||
});
|
||||
expect(commander.arg0).toEqual('arg0ENVValue');
|
||||
expect(commander.arg1).toEqual('arg1ENVValue');
|
||||
@@ -74,13 +61,13 @@ describe('commander additions', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should load properly use args over env', done => {
|
||||
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',
|
||||
PROGRAM_ARG_1: 'arg1ENVValue',
|
||||
PROGRAM_ARG_2: '4',
|
||||
PROGRAM_ARG_4: 'arg4ENVValue',
|
||||
commander.parse(['node','./CLI.spec.js','--arg0', 'arg0Value', '--arg4', ''], {
|
||||
'PROGRAM_ARG_0': 'arg0ENVValue',
|
||||
'PROGRAM_ARG_1': 'arg1ENVValue',
|
||||
'PROGRAM_ARG_2': '4',
|
||||
'PROGRAM_ARG_4': 'arg4ENVValue'
|
||||
});
|
||||
expect(commander.arg0).toEqual('arg0Value');
|
||||
expect(commander.arg1).toEqual('arg1ENVValue');
|
||||
@@ -89,28 +76,24 @@ describe('commander additions', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should fail in action as port is invalid', done => {
|
||||
it('should fail in action as port is invalid', (done) => {
|
||||
commander.loadDefinitions(testDefinitions);
|
||||
expect(() => {
|
||||
commander.parse(['node', './CLI.spec.js', '--arg0', 'arg0Value'], {
|
||||
PROGRAM_ARG_0: 'arg0ENVValue',
|
||||
PROGRAM_ARG_1: 'arg1ENVValue',
|
||||
PROGRAM_ARG_2: 'hello',
|
||||
expect(()=> {
|
||||
commander.parse(['node','./CLI.spec.js','--arg0', 'arg0Value'], {
|
||||
'PROGRAM_ARG_0': 'arg0ENVValue',
|
||||
'PROGRAM_ARG_1': 'arg1ENVValue',
|
||||
'PROGRAM_ARG_2': 'hello',
|
||||
});
|
||||
}).toThrow('arg2 is invalid');
|
||||
done();
|
||||
});
|
||||
|
||||
it('should not override config.json', done => {
|
||||
spyOn(console, 'log').and.callFake(() => {});
|
||||
it('should not override config.json', (done) => {
|
||||
commander.loadDefinitions(testDefinitions);
|
||||
commander.parse(
|
||||
['node', './CLI.spec.js', '--arg0', 'arg0Value', './spec/configs/CLIConfig.json'],
|
||||
{
|
||||
PROGRAM_ARG_0: 'arg0ENVValue',
|
||||
PROGRAM_ARG_1: 'arg1ENVValue',
|
||||
}
|
||||
);
|
||||
commander.parse(['node','./CLI.spec.js','--arg0', 'arg0Value', './spec/configs/CLIConfig.json'], {
|
||||
'PROGRAM_ARG_0': 'arg0ENVValue',
|
||||
'PROGRAM_ARG_1': 'arg1ENVValue',
|
||||
});
|
||||
const options = commander.getOptions();
|
||||
expect(options.arg2).toBe(8888);
|
||||
expect(options.arg3).toBe('hello'); //config value
|
||||
@@ -118,30 +101,26 @@ describe('commander additions', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should fail with invalid values in JSON', done => {
|
||||
it('should fail with invalid values in JSON', (done) => {
|
||||
commander.loadDefinitions(testDefinitions);
|
||||
expect(() => {
|
||||
commander.parse(
|
||||
['node', './CLI.spec.js', '--arg0', 'arg0Value', './spec/configs/CLIConfigFail.json'],
|
||||
{
|
||||
PROGRAM_ARG_0: 'arg0ENVValue',
|
||||
PROGRAM_ARG_1: 'arg1ENVValue',
|
||||
}
|
||||
);
|
||||
commander.parse(['node','./CLI.spec.js','--arg0', 'arg0Value', './spec/configs/CLIConfigFail.json'], {
|
||||
'PROGRAM_ARG_0': 'arg0ENVValue',
|
||||
'PROGRAM_ARG_1': 'arg1ENVValue',
|
||||
});
|
||||
}).toThrow('arg2 is invalid');
|
||||
done();
|
||||
});
|
||||
|
||||
it('should fail when too many apps are set', done => {
|
||||
it('should fail when too many apps are set', (done) => {
|
||||
commander.loadDefinitions(testDefinitions);
|
||||
expect(() => {
|
||||
commander.parse(['node', './CLI.spec.js', './spec/configs/CLIConfigFailTooManyApps.json']);
|
||||
commander.parse(['node','./CLI.spec.js','./spec/configs/CLIConfigFailTooManyApps.json']);
|
||||
}).toThrow('Multiple apps are not supported');
|
||||
done();
|
||||
});
|
||||
|
||||
it('should load config from apps', done => {
|
||||
spyOn(console, 'log').and.callFake(() => {});
|
||||
it('should load config from apps', (done) => {
|
||||
commander.loadDefinitions(testDefinitions);
|
||||
commander.parse(['node', './CLI.spec.js', './spec/configs/CLIConfigApps.json']);
|
||||
const options = commander.getOptions();
|
||||
@@ -152,7 +131,7 @@ describe('commander additions', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should fail when passing an invalid arguement', done => {
|
||||
it('should fail when passing an invalid arguement', (done) => {
|
||||
commander.loadDefinitions(testDefinitions);
|
||||
expect(() => {
|
||||
commander.parse(['node', './CLI.spec.js', './spec/configs/CLIConfigUnknownArg.json']);
|
||||
@@ -178,12 +157,6 @@ describe('definitions', () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw when using deprecated facebookAppIds', () => {
|
||||
expect(() => {
|
||||
definitions.facebookAppIds.action();
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('LiveQuery definitions', () => {
|
||||
@@ -194,7 +167,7 @@ describe('LiveQuery definitions', () => {
|
||||
if (typeof definition.env !== 'undefined') {
|
||||
expect(typeof definition.env).toBe('string');
|
||||
}
|
||||
expect(typeof definition.help).toBe('string', `help for ${key} should be a string`);
|
||||
expect(typeof definition.help).toBe('string');
|
||||
if (typeof definition.required !== 'undefined') {
|
||||
expect(typeof definition.required).toBe('boolean');
|
||||
}
|
||||
@@ -204,84 +177,3 @@ describe('LiveQuery definitions', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('execution', () => {
|
||||
const binPath = path.resolve(__dirname, '../bin/parse-server');
|
||||
let childProcess;
|
||||
|
||||
afterEach(async () => {
|
||||
if (childProcess) {
|
||||
childProcess.kill();
|
||||
}
|
||||
});
|
||||
|
||||
it('shoud start Parse Server', done => {
|
||||
childProcess = spawn(binPath, [
|
||||
'--appId',
|
||||
'test',
|
||||
'--masterKey',
|
||||
'test',
|
||||
'--databaseURI',
|
||||
'mongodb://localhost/test',
|
||||
]);
|
||||
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('shoud start Parse Server with GraphQL', done => {
|
||||
childProcess = spawn(binPath, [
|
||||
'--appId',
|
||||
'test',
|
||||
'--masterKey',
|
||||
'test',
|
||||
'--databaseURI',
|
||||
'mongodb://localhost/test',
|
||||
'--mountGraphQL',
|
||||
]);
|
||||
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('shoud start Parse Server with GraphQL and Playground', done => {
|
||||
childProcess = spawn(binPath, [
|
||||
'--appId',
|
||||
'test',
|
||||
'--masterKey',
|
||||
'test',
|
||||
'--databaseURI',
|
||||
'mongodb://localhost/test',
|
||||
'--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());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
const CacheController = require('../lib/Controllers/CacheController.js').default;
|
||||
var CacheController = require('../src/Controllers/CacheController.js').default;
|
||||
|
||||
describe('CacheController', function () {
|
||||
let FakeCacheAdapter;
|
||||
const FakeAppID = 'foo';
|
||||
const KEY = 'hello';
|
||||
describe('CacheController', function() {
|
||||
var FakeCacheAdapter;
|
||||
var FakeAppID = 'foo';
|
||||
var KEY = 'hello';
|
||||
|
||||
beforeEach(() => {
|
||||
FakeCacheAdapter = {
|
||||
get: () => Promise.resolve(null),
|
||||
put: jasmine.createSpy('put'),
|
||||
del: jasmine.createSpy('del'),
|
||||
clear: jasmine.createSpy('clear'),
|
||||
};
|
||||
clear: jasmine.createSpy('clear')
|
||||
}
|
||||
|
||||
spyOn(FakeCacheAdapter, 'get').and.callThrough();
|
||||
});
|
||||
|
||||
it('should expose role and user caches', done => {
|
||||
const cache = new CacheController(FakeCacheAdapter, FakeAppID);
|
||||
|
||||
it('should expose role and user caches', (done) => {
|
||||
var cache = new CacheController(FakeCacheAdapter, FakeAppID);
|
||||
|
||||
expect(cache.role).not.toEqual(null);
|
||||
expect(cache.role.get).not.toEqual(null);
|
||||
@@ -27,26 +28,27 @@ describe('CacheController', function () {
|
||||
done();
|
||||
});
|
||||
|
||||
['role', 'user'].forEach(cacheName => {
|
||||
|
||||
['role', 'user'].forEach((cacheName) => {
|
||||
it('should prefix ' + cacheName + ' cache', () => {
|
||||
const cache = new CacheController(FakeCacheAdapter, FakeAppID)[cacheName];
|
||||
var cache = new CacheController(FakeCacheAdapter, FakeAppID)[cacheName];
|
||||
|
||||
cache.put(KEY, 'world');
|
||||
const firstPut = FakeCacheAdapter.put.calls.first();
|
||||
var firstPut = FakeCacheAdapter.put.calls.first();
|
||||
expect(firstPut.args[0]).toEqual([FakeAppID, cacheName, KEY].join(':'));
|
||||
|
||||
cache.get(KEY);
|
||||
const firstGet = FakeCacheAdapter.get.calls.first();
|
||||
var firstGet = FakeCacheAdapter.get.calls.first();
|
||||
expect(firstGet.args[0]).toEqual([FakeAppID, cacheName, KEY].join(':'));
|
||||
|
||||
cache.del(KEY);
|
||||
const firstDel = FakeCacheAdapter.del.calls.first();
|
||||
var firstDel = FakeCacheAdapter.del.calls.first();
|
||||
expect(firstDel.args[0]).toEqual([FakeAppID, cacheName, KEY].join(':'));
|
||||
});
|
||||
});
|
||||
|
||||
it('should clear the entire cache', () => {
|
||||
const cache = new CacheController(FakeCacheAdapter, FakeAppID);
|
||||
var cache = new CacheController(FakeCacheAdapter, FakeAppID);
|
||||
|
||||
cache.clear();
|
||||
expect(FakeCacheAdapter.clear.calls.count()).toEqual(1);
|
||||
@@ -58,13 +60,15 @@ describe('CacheController', function () {
|
||||
expect(FakeCacheAdapter.clear.calls.count()).toEqual(3);
|
||||
});
|
||||
|
||||
it('should handle cache rejections', done => {
|
||||
it('should handle cache rejections', (done) => {
|
||||
|
||||
FakeCacheAdapter.get = () => Promise.reject();
|
||||
|
||||
const cache = new CacheController(FakeCacheAdapter, FakeAppID);
|
||||
var cache = new CacheController(FakeCacheAdapter, FakeAppID);
|
||||
|
||||
cache.get('foo').then(done, () => {
|
||||
fail('Promise should not be rejected.');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
+111
-110
@@ -1,120 +1,121 @@
|
||||
const Client = require('../lib/LiveQuery/Client').Client;
|
||||
const ParseWebSocket = require('../lib/LiveQuery/ParseWebSocketServer').ParseWebSocket;
|
||||
var Client = require('../src/LiveQuery/Client').Client;
|
||||
var ParseWebSocket = require('../src/LiveQuery/ParseWebSocketServer').ParseWebSocket;
|
||||
|
||||
describe('Client', function () {
|
||||
it('can be initialized', function () {
|
||||
const parseWebSocket = new ParseWebSocket({});
|
||||
const client = new Client(1, parseWebSocket);
|
||||
describe('Client', function() {
|
||||
it('can be initialized', function() {
|
||||
var parseWebSocket = new ParseWebSocket({});
|
||||
var client = new Client(1, parseWebSocket);
|
||||
|
||||
expect(client.id).toBe(1);
|
||||
expect(client.parseWebSocket).toBe(parseWebSocket);
|
||||
expect(client.subscriptionInfos.size).toBe(0);
|
||||
});
|
||||
|
||||
it('can push response', function () {
|
||||
const parseWebSocket = {
|
||||
send: jasmine.createSpy('send'),
|
||||
it('can push response', function() {
|
||||
var parseWebSocket = {
|
||||
send: jasmine.createSpy('send')
|
||||
};
|
||||
Client.pushResponse(parseWebSocket, 'message');
|
||||
|
||||
expect(parseWebSocket.send).toHaveBeenCalledWith('message');
|
||||
});
|
||||
|
||||
it('can push error', function () {
|
||||
const parseWebSocket = {
|
||||
send: jasmine.createSpy('send'),
|
||||
it('can push error', function() {
|
||||
var parseWebSocket = {
|
||||
send: jasmine.createSpy('send')
|
||||
};
|
||||
Client.pushError(parseWebSocket, 1, 'error', true);
|
||||
|
||||
const lastCall = parseWebSocket.send.calls.first();
|
||||
const messageJSON = JSON.parse(lastCall.args[0]);
|
||||
var lastCall = parseWebSocket.send.calls.first();
|
||||
var messageJSON = JSON.parse(lastCall.args[0]);
|
||||
expect(messageJSON.op).toBe('error');
|
||||
expect(messageJSON.error).toBe('error');
|
||||
expect(messageJSON.code).toBe(1);
|
||||
expect(messageJSON.reconnect).toBe(true);
|
||||
});
|
||||
|
||||
it('can add subscription information', function () {
|
||||
const subscription = {};
|
||||
const fields = ['test'];
|
||||
const subscriptionInfo = {
|
||||
it('can add subscription information', function() {
|
||||
var subscription = {};
|
||||
var fields = ['test'];
|
||||
var subscriptionInfo = {
|
||||
subscription: subscription,
|
||||
fields: fields,
|
||||
};
|
||||
const client = new Client(1, {});
|
||||
fields: fields
|
||||
}
|
||||
var client = new Client(1, {});
|
||||
client.addSubscriptionInfo(1, subscriptionInfo);
|
||||
|
||||
expect(client.subscriptionInfos.size).toBe(1);
|
||||
expect(client.subscriptionInfos.get(1)).toBe(subscriptionInfo);
|
||||
});
|
||||
|
||||
it('can get subscription information', function () {
|
||||
const subscription = {};
|
||||
const fields = ['test'];
|
||||
const subscriptionInfo = {
|
||||
it('can get subscription information', function() {
|
||||
var subscription = {};
|
||||
var fields = ['test'];
|
||||
var subscriptionInfo = {
|
||||
subscription: subscription,
|
||||
fields: fields,
|
||||
};
|
||||
const client = new Client(1, {});
|
||||
fields: fields
|
||||
}
|
||||
var client = new Client(1, {});
|
||||
client.addSubscriptionInfo(1, subscriptionInfo);
|
||||
const subscriptionInfoAgain = client.getSubscriptionInfo(1);
|
||||
var subscriptionInfoAgain = client.getSubscriptionInfo(1);
|
||||
|
||||
expect(subscriptionInfoAgain).toBe(subscriptionInfo);
|
||||
});
|
||||
|
||||
it('can delete subscription information', function () {
|
||||
const subscription = {};
|
||||
const fields = ['test'];
|
||||
const subscriptionInfo = {
|
||||
it('can delete subscription information', function() {
|
||||
var subscription = {};
|
||||
var fields = ['test'];
|
||||
var subscriptionInfo = {
|
||||
subscription: subscription,
|
||||
fields: fields,
|
||||
};
|
||||
const client = new Client(1, {});
|
||||
fields: fields
|
||||
}
|
||||
var client = new Client(1, {});
|
||||
client.addSubscriptionInfo(1, subscriptionInfo);
|
||||
client.deleteSubscriptionInfo(1);
|
||||
|
||||
expect(client.subscriptionInfos.size).toBe(0);
|
||||
});
|
||||
|
||||
it('can generate ParseObject JSON with null selected field', function () {
|
||||
const parseObjectJSON = {
|
||||
key: 'value',
|
||||
|
||||
it('can generate ParseObject JSON with null selected field', function() {
|
||||
var parseObjectJSON = {
|
||||
key : 'value',
|
||||
className: 'test',
|
||||
objectId: 'test',
|
||||
updatedAt: '2015-12-07T21:27:13.746Z',
|
||||
createdAt: '2015-12-07T21:27:13.746Z',
|
||||
ACL: 'test',
|
||||
};
|
||||
const client = new Client(1, {});
|
||||
var client = new Client(1, {});
|
||||
|
||||
expect(client._toJSONWithFields(parseObjectJSON, null)).toBe(parseObjectJSON);
|
||||
});
|
||||
|
||||
it('can generate ParseObject JSON with undefined selected field', function () {
|
||||
const parseObjectJSON = {
|
||||
key: 'value',
|
||||
it('can generate ParseObject JSON with undefined selected field', function() {
|
||||
var parseObjectJSON = {
|
||||
key : 'value',
|
||||
className: 'test',
|
||||
objectId: 'test',
|
||||
updatedAt: '2015-12-07T21:27:13.746Z',
|
||||
createdAt: '2015-12-07T21:27:13.746Z',
|
||||
ACL: 'test',
|
||||
};
|
||||
const client = new Client(1, {});
|
||||
var client = new Client(1, {});
|
||||
|
||||
expect(client._toJSONWithFields(parseObjectJSON, undefined)).toBe(parseObjectJSON);
|
||||
});
|
||||
|
||||
it('can generate ParseObject JSON with selected fields', function () {
|
||||
const parseObjectJSON = {
|
||||
key: 'value',
|
||||
it('can generate ParseObject JSON with selected fields', function() {
|
||||
var parseObjectJSON = {
|
||||
key : 'value',
|
||||
className: 'test',
|
||||
objectId: 'test',
|
||||
updatedAt: '2015-12-07T21:27:13.746Z',
|
||||
createdAt: '2015-12-07T21:27:13.746Z',
|
||||
ACL: 'test',
|
||||
test: 'test',
|
||||
test: 'test'
|
||||
};
|
||||
const client = new Client(1, {});
|
||||
var client = new Client(1, {});
|
||||
|
||||
expect(client._toJSONWithFields(parseObjectJSON, ['test'])).toEqual({
|
||||
className: 'test',
|
||||
@@ -122,22 +123,22 @@ describe('Client', function () {
|
||||
updatedAt: '2015-12-07T21:27:13.746Z',
|
||||
createdAt: '2015-12-07T21:27:13.746Z',
|
||||
ACL: 'test',
|
||||
test: 'test',
|
||||
test: 'test'
|
||||
});
|
||||
});
|
||||
|
||||
it('can generate ParseObject JSON with nonexistent selected fields', function () {
|
||||
const parseObjectJSON = {
|
||||
key: 'value',
|
||||
it('can generate ParseObject JSON with nonexistent selected fields', function() {
|
||||
var parseObjectJSON = {
|
||||
key : 'value',
|
||||
className: 'test',
|
||||
objectId: 'test',
|
||||
updatedAt: '2015-12-07T21:27:13.746Z',
|
||||
createdAt: '2015-12-07T21:27:13.746Z',
|
||||
ACL: 'test',
|
||||
test: 'test',
|
||||
test: 'test'
|
||||
};
|
||||
const client = new Client(1, {});
|
||||
const limitedParseObject = client._toJSONWithFields(parseObjectJSON, ['name']);
|
||||
var client = new Client(1, {});
|
||||
var limitedParseObject = client._toJSONWithFields(parseObjectJSON, ['name']);
|
||||
|
||||
expect(limitedParseObject).toEqual({
|
||||
className: 'test',
|
||||
@@ -149,137 +150,137 @@ describe('Client', function () {
|
||||
expect('name' in limitedParseObject).toBe(false);
|
||||
});
|
||||
|
||||
it('can push connect response', function () {
|
||||
const parseWebSocket = {
|
||||
send: jasmine.createSpy('send'),
|
||||
it('can push connect response', function() {
|
||||
var parseWebSocket = {
|
||||
send: jasmine.createSpy('send')
|
||||
};
|
||||
const client = new Client(1, parseWebSocket);
|
||||
var client = new Client(1, parseWebSocket);
|
||||
client.pushConnect();
|
||||
|
||||
const lastCall = parseWebSocket.send.calls.first();
|
||||
const messageJSON = JSON.parse(lastCall.args[0]);
|
||||
var lastCall = parseWebSocket.send.calls.first();
|
||||
var messageJSON = JSON.parse(lastCall.args[0]);
|
||||
expect(messageJSON.op).toBe('connected');
|
||||
expect(messageJSON.clientId).toBe(1);
|
||||
});
|
||||
|
||||
it('can push subscribe response', function () {
|
||||
const parseWebSocket = {
|
||||
send: jasmine.createSpy('send'),
|
||||
it('can push subscribe response', function() {
|
||||
var parseWebSocket = {
|
||||
send: jasmine.createSpy('send')
|
||||
};
|
||||
const client = new Client(1, parseWebSocket);
|
||||
var client = new Client(1, parseWebSocket);
|
||||
client.pushSubscribe(2);
|
||||
|
||||
const lastCall = parseWebSocket.send.calls.first();
|
||||
const messageJSON = JSON.parse(lastCall.args[0]);
|
||||
var lastCall = parseWebSocket.send.calls.first();
|
||||
var messageJSON = JSON.parse(lastCall.args[0]);
|
||||
expect(messageJSON.op).toBe('subscribed');
|
||||
expect(messageJSON.clientId).toBe(1);
|
||||
expect(messageJSON.requestId).toBe(2);
|
||||
});
|
||||
|
||||
it('can push unsubscribe response', function () {
|
||||
const parseWebSocket = {
|
||||
send: jasmine.createSpy('send'),
|
||||
it('can push unsubscribe response', function() {
|
||||
var parseWebSocket = {
|
||||
send: jasmine.createSpy('send')
|
||||
};
|
||||
const client = new Client(1, parseWebSocket);
|
||||
var client = new Client(1, parseWebSocket);
|
||||
client.pushUnsubscribe(2);
|
||||
|
||||
const lastCall = parseWebSocket.send.calls.first();
|
||||
const messageJSON = JSON.parse(lastCall.args[0]);
|
||||
var lastCall = parseWebSocket.send.calls.first();
|
||||
var messageJSON = JSON.parse(lastCall.args[0]);
|
||||
expect(messageJSON.op).toBe('unsubscribed');
|
||||
expect(messageJSON.clientId).toBe(1);
|
||||
expect(messageJSON.requestId).toBe(2);
|
||||
});
|
||||
|
||||
it('can push create response', function () {
|
||||
const parseObjectJSON = {
|
||||
key: 'value',
|
||||
it('can push create response', function() {
|
||||
var parseObjectJSON = {
|
||||
key : 'value',
|
||||
className: 'test',
|
||||
objectId: 'test',
|
||||
updatedAt: '2015-12-07T21:27:13.746Z',
|
||||
createdAt: '2015-12-07T21:27:13.746Z',
|
||||
ACL: 'test',
|
||||
test: 'test',
|
||||
test: 'test'
|
||||
};
|
||||
const parseWebSocket = {
|
||||
send: jasmine.createSpy('send'),
|
||||
var parseWebSocket = {
|
||||
send: jasmine.createSpy('send')
|
||||
};
|
||||
const client = new Client(1, parseWebSocket);
|
||||
var client = new Client(1, parseWebSocket);
|
||||
client.pushCreate(2, parseObjectJSON);
|
||||
|
||||
const lastCall = parseWebSocket.send.calls.first();
|
||||
const messageJSON = JSON.parse(lastCall.args[0]);
|
||||
var lastCall = parseWebSocket.send.calls.first();
|
||||
var messageJSON = JSON.parse(lastCall.args[0]);
|
||||
expect(messageJSON.op).toBe('create');
|
||||
expect(messageJSON.clientId).toBe(1);
|
||||
expect(messageJSON.requestId).toBe(2);
|
||||
expect(messageJSON.object).toEqual(parseObjectJSON);
|
||||
});
|
||||
|
||||
it('can push enter response', function () {
|
||||
const parseObjectJSON = {
|
||||
key: 'value',
|
||||
it('can push enter response', function() {
|
||||
var parseObjectJSON = {
|
||||
key : 'value',
|
||||
className: 'test',
|
||||
objectId: 'test',
|
||||
updatedAt: '2015-12-07T21:27:13.746Z',
|
||||
createdAt: '2015-12-07T21:27:13.746Z',
|
||||
ACL: 'test',
|
||||
test: 'test',
|
||||
test: 'test'
|
||||
};
|
||||
const parseWebSocket = {
|
||||
send: jasmine.createSpy('send'),
|
||||
var parseWebSocket = {
|
||||
send: jasmine.createSpy('send')
|
||||
};
|
||||
const client = new Client(1, parseWebSocket);
|
||||
var client = new Client(1, parseWebSocket);
|
||||
client.pushEnter(2, parseObjectJSON);
|
||||
|
||||
const lastCall = parseWebSocket.send.calls.first();
|
||||
const messageJSON = JSON.parse(lastCall.args[0]);
|
||||
var lastCall = parseWebSocket.send.calls.first();
|
||||
var messageJSON = JSON.parse(lastCall.args[0]);
|
||||
expect(messageJSON.op).toBe('enter');
|
||||
expect(messageJSON.clientId).toBe(1);
|
||||
expect(messageJSON.requestId).toBe(2);
|
||||
expect(messageJSON.object).toEqual(parseObjectJSON);
|
||||
});
|
||||
|
||||
it('can push update response', function () {
|
||||
const parseObjectJSON = {
|
||||
key: 'value',
|
||||
it('can push update response', function() {
|
||||
var parseObjectJSON = {
|
||||
key : 'value',
|
||||
className: 'test',
|
||||
objectId: 'test',
|
||||
updatedAt: '2015-12-07T21:27:13.746Z',
|
||||
createdAt: '2015-12-07T21:27:13.746Z',
|
||||
ACL: 'test',
|
||||
test: 'test',
|
||||
test: 'test'
|
||||
};
|
||||
const parseWebSocket = {
|
||||
send: jasmine.createSpy('send'),
|
||||
var parseWebSocket = {
|
||||
send: jasmine.createSpy('send')
|
||||
};
|
||||
const client = new Client(1, parseWebSocket);
|
||||
var client = new Client(1, parseWebSocket);
|
||||
client.pushUpdate(2, parseObjectJSON);
|
||||
|
||||
const lastCall = parseWebSocket.send.calls.first();
|
||||
const messageJSON = JSON.parse(lastCall.args[0]);
|
||||
var lastCall = parseWebSocket.send.calls.first();
|
||||
var messageJSON = JSON.parse(lastCall.args[0]);
|
||||
expect(messageJSON.op).toBe('update');
|
||||
expect(messageJSON.clientId).toBe(1);
|
||||
expect(messageJSON.requestId).toBe(2);
|
||||
expect(messageJSON.object).toEqual(parseObjectJSON);
|
||||
});
|
||||
|
||||
it('can push leave response', function () {
|
||||
const parseObjectJSON = {
|
||||
key: 'value',
|
||||
it('can push leave response', function() {
|
||||
var parseObjectJSON = {
|
||||
key : 'value',
|
||||
className: 'test',
|
||||
objectId: 'test',
|
||||
updatedAt: '2015-12-07T21:27:13.746Z',
|
||||
createdAt: '2015-12-07T21:27:13.746Z',
|
||||
ACL: 'test',
|
||||
test: 'test',
|
||||
test: 'test'
|
||||
};
|
||||
const parseWebSocket = {
|
||||
send: jasmine.createSpy('send'),
|
||||
var parseWebSocket = {
|
||||
send: jasmine.createSpy('send')
|
||||
};
|
||||
const client = new Client(1, parseWebSocket);
|
||||
var client = new Client(1, parseWebSocket);
|
||||
client.pushLeave(2, parseObjectJSON);
|
||||
|
||||
const lastCall = parseWebSocket.send.calls.first();
|
||||
const messageJSON = JSON.parse(lastCall.args[0]);
|
||||
var lastCall = parseWebSocket.send.calls.first();
|
||||
var messageJSON = JSON.parse(lastCall.args[0]);
|
||||
expect(messageJSON.op).toBe('leave');
|
||||
expect(messageJSON.clientId).toBe(1);
|
||||
expect(messageJSON.requestId).toBe(2);
|
||||
|
||||
+19
-27
@@ -1,49 +1,41 @@
|
||||
const ClientSDK = require('../lib/ClientSDK');
|
||||
var ClientSDK = require('../src/ClientSDK');
|
||||
|
||||
describe('ClientSDK', () => {
|
||||
it('should properly parse the SDK versions', () => {
|
||||
const clientSDKFromVersion = ClientSDK.fromString;
|
||||
expect(clientSDKFromVersion('i1.1.1')).toEqual({
|
||||
sdk: 'i',
|
||||
version: '1.1.1',
|
||||
version: '1.1.1'
|
||||
});
|
||||
expect(clientSDKFromVersion('i1')).toEqual({
|
||||
sdk: 'i',
|
||||
version: '1',
|
||||
version: '1'
|
||||
});
|
||||
expect(clientSDKFromVersion('apple-tv1.13.0')).toEqual({
|
||||
sdk: 'apple-tv',
|
||||
version: '1.13.0',
|
||||
version: '1.13.0'
|
||||
});
|
||||
expect(clientSDKFromVersion('js1.9.0')).toEqual({
|
||||
sdk: 'js',
|
||||
version: '1.9.0',
|
||||
version: '1.9.0'
|
||||
});
|
||||
});
|
||||
|
||||
it('should properly sastisfy', () => {
|
||||
expect(
|
||||
ClientSDK.compatible({
|
||||
js: '>=1.9.0',
|
||||
})('js1.9.0')
|
||||
).toBe(true);
|
||||
expect(ClientSDK.compatible({
|
||||
js: '>=1.9.0'
|
||||
})("js1.9.0")).toBe(true);
|
||||
|
||||
expect(
|
||||
ClientSDK.compatible({
|
||||
js: '>=1.9.0',
|
||||
})('js2.0.0')
|
||||
).toBe(true);
|
||||
expect(ClientSDK.compatible({
|
||||
js: '>=1.9.0'
|
||||
})("js2.0.0")).toBe(true);
|
||||
|
||||
expect(
|
||||
ClientSDK.compatible({
|
||||
js: '>=1.9.0',
|
||||
})('js1.8.0')
|
||||
).toBe(false);
|
||||
expect(ClientSDK.compatible({
|
||||
js: '>=1.9.0'
|
||||
})("js1.8.0")).toBe(false);
|
||||
|
||||
expect(
|
||||
ClientSDK.compatible({
|
||||
js: '>=1.9.0',
|
||||
})(undefined)
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
expect(ClientSDK.compatible({
|
||||
js: '>=1.9.0'
|
||||
})(undefined)).toBe(true);
|
||||
})
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+895
-2582
File diff suppressed because it is too large
Load Diff
+150
-184
@@ -1,106 +1,106 @@
|
||||
const LoggerController = require('../lib/Controllers/LoggerController').LoggerController;
|
||||
const WinstonLoggerAdapter = require('../lib/Adapters/Logger/WinstonLoggerAdapter')
|
||||
.WinstonLoggerAdapter;
|
||||
const fs = require('fs');
|
||||
const Config = require('../lib/Config');
|
||||
'use strict';
|
||||
var LoggerController = require('../src/Controllers/LoggerController').LoggerController;
|
||||
var WinstonLoggerAdapter = require('../src/Adapters/Logger/WinstonLoggerAdapter').WinstonLoggerAdapter;
|
||||
|
||||
const fs = require('fs');
|
||||
const loremFile = __dirname + '/support/lorem.txt';
|
||||
|
||||
describe('Cloud Code Logger', () => {
|
||||
describe("Cloud Code Logger", () => {
|
||||
let user;
|
||||
let spy;
|
||||
beforeEach(async () => {
|
||||
|
||||
beforeEach(done => {
|
||||
Parse.User.enableUnsafeCurrentUser();
|
||||
return reconfigureServer({
|
||||
// useful to flip to false for fine tuning :).
|
||||
// useful to flip to false for fine tuning :).
|
||||
silent: true,
|
||||
})
|
||||
.then(() => {
|
||||
return Parse.User.signUp('tester', 'abc')
|
||||
.catch(() => {})
|
||||
.then(loggedInUser => (user = loggedInUser))
|
||||
.then(() => Parse.User.logIn(user.get('username'), 'abc'));
|
||||
})
|
||||
.then(() => {
|
||||
spy = spyOn(Config.get('test').loggerController.adapter, 'log').and.callThrough();
|
||||
});
|
||||
}).then(() => {
|
||||
return Parse.User.signUp('tester', 'abc')
|
||||
.then(loggedInUser => user = loggedInUser)
|
||||
.then(() => Parse.User.logIn(user.get('username'), 'abc'))
|
||||
.then(() => done())
|
||||
});
|
||||
});
|
||||
|
||||
// Note that helpers takes care of logout.
|
||||
// see helpers.js:afterEach
|
||||
// Note that helpers takes care of logout.
|
||||
// see helpers.js:afterEach
|
||||
|
||||
it('should expose log to functions', () => {
|
||||
const spy = spyOn(Config.get('test').loggerController, 'log').and.callThrough();
|
||||
Parse.Cloud.define('loggerTest', req => {
|
||||
it("should expose log to functions", done => {
|
||||
var logController = new LoggerController(new WinstonLoggerAdapter());
|
||||
|
||||
Parse.Cloud.define("loggerTest", (req, res) => {
|
||||
req.log.info('logTest', 'info log', { info: 'some log' });
|
||||
req.log.error('logTest', 'error log', { error: 'there was an error' });
|
||||
return {};
|
||||
res.success({});
|
||||
});
|
||||
|
||||
return Parse.Cloud.run('loggerTest').then(() => {
|
||||
expect(spy).toHaveBeenCalledTimes(3);
|
||||
const cloudFunctionMessage = spy.calls.all()[2];
|
||||
const errorMessage = spy.calls.all()[1];
|
||||
const infoMessage = spy.calls.all()[0];
|
||||
expect(cloudFunctionMessage.args[0]).toBe('info');
|
||||
expect(cloudFunctionMessage.args[1][1].params).toEqual({});
|
||||
expect(cloudFunctionMessage.args[1][0]).toMatch(
|
||||
/Ran cloud function loggerTest for user [^ ]* with:\n {2}Input: {}\n {2}Result: {}/
|
||||
);
|
||||
expect(cloudFunctionMessage.args[1][1].functionName).toEqual('loggerTest');
|
||||
expect(errorMessage.args[0]).toBe('error');
|
||||
expect(errorMessage.args[1][2].error).toBe('there was an error');
|
||||
expect(errorMessage.args[1][0]).toBe('logTest');
|
||||
expect(errorMessage.args[1][1]).toBe('error log');
|
||||
expect(infoMessage.args[0]).toBe('info');
|
||||
expect(infoMessage.args[1][2].info).toBe('some log');
|
||||
expect(infoMessage.args[1][0]).toBe('logTest');
|
||||
expect(infoMessage.args[1][1]).toBe('info log');
|
||||
Parse.Cloud.run('loggerTest').then(() => {
|
||||
return logController.getLogs({ from: Date.now() - 500, size: 1000 });
|
||||
}).then((res) => {
|
||||
expect(res.length).not.toBe(0);
|
||||
const lastLogs = res.slice(0, 3);
|
||||
const cloudFunctionMessage = lastLogs[0];
|
||||
const errorMessage = lastLogs[1];
|
||||
const infoMessage = lastLogs[2];
|
||||
expect(cloudFunctionMessage.level).toBe('info');
|
||||
expect(cloudFunctionMessage.params).toEqual({});
|
||||
expect(cloudFunctionMessage.message).toMatch(/Ran cloud function loggerTest for user [^ ]* with:\n {2}Input: {}\n {2}Result: {}/);
|
||||
expect(cloudFunctionMessage.functionName).toEqual('loggerTest');
|
||||
expect(errorMessage.level).toBe('error');
|
||||
expect(errorMessage.error).toBe('there was an error');
|
||||
expect(errorMessage.message).toBe('logTest error log');
|
||||
expect(infoMessage.level).toBe('info');
|
||||
expect(infoMessage.info).toBe('some log');
|
||||
expect(infoMessage.message).toBe('logTest info log');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('trigger should obfuscate password', done => {
|
||||
Parse.Cloud.beforeSave(Parse.User, req => {
|
||||
return req.object;
|
||||
const logController = new LoggerController(new WinstonLoggerAdapter());
|
||||
|
||||
Parse.Cloud.beforeSave(Parse.User, (req, res) => {
|
||||
res.success(req.object);
|
||||
});
|
||||
|
||||
Parse.User.signUp('tester123', 'abc')
|
||||
.then(() => {
|
||||
const entry = spy.calls.mostRecent().args;
|
||||
expect(entry[1]).not.toMatch(/password":"abc/);
|
||||
expect(entry[1]).toMatch(/\*\*\*\*\*\*\*\*/);
|
||||
done();
|
||||
})
|
||||
.then(null, e => done.fail(e));
|
||||
.then(() => logController.getLogs({ from: Date.now() - 500, size: 1000 }))
|
||||
.then((res) => {
|
||||
const entry = res[0];
|
||||
expect(entry.message).not.toMatch(/password":"abc/);
|
||||
expect(entry.message).toMatch(/\*\*\*\*\*\*\*\*/);
|
||||
done();
|
||||
})
|
||||
.then(null, e => done.fail(e));
|
||||
});
|
||||
|
||||
it('should expose log to trigger', done => {
|
||||
Parse.Cloud.beforeSave('MyObject', req => {
|
||||
it("should expose log to trigger", (done) => {
|
||||
var logController = new LoggerController(new WinstonLoggerAdapter());
|
||||
|
||||
Parse.Cloud.beforeSave("MyObject", (req, res) => {
|
||||
req.log.info('beforeSave MyObject', 'info log', { info: 'some log' });
|
||||
req.log.error('beforeSave MyObject', 'error log', {
|
||||
error: 'there was an error',
|
||||
});
|
||||
return {};
|
||||
req.log.error('beforeSave MyObject', 'error log', { error: 'there was an error' });
|
||||
res.success({});
|
||||
});
|
||||
|
||||
const obj = new Parse.Object('MyObject');
|
||||
obj.save().then(() => {
|
||||
const lastCalls = spy.calls.all().reverse();
|
||||
const cloudTriggerMessage = lastCalls[0].args;
|
||||
const errorMessage = lastCalls[1].args;
|
||||
const infoMessage = lastCalls[2].args;
|
||||
expect(cloudTriggerMessage[0]).toBe('info');
|
||||
expect(cloudTriggerMessage[2].triggerType).toEqual('beforeSave');
|
||||
expect(cloudTriggerMessage[1]).toMatch(
|
||||
/beforeSave triggered for MyObject for user [^ ]*\n {2}Input: {}\n {2}Result: {"object":{}}/
|
||||
);
|
||||
expect(cloudTriggerMessage[2].user).toBe(user.id);
|
||||
expect(errorMessage[0]).toBe('error');
|
||||
expect(errorMessage[3].error).toBe('there was an error');
|
||||
expect(errorMessage[1] + ' ' + errorMessage[2]).toBe('beforeSave MyObject error log');
|
||||
expect(infoMessage[0]).toBe('info');
|
||||
expect(infoMessage[3].info).toBe('some log');
|
||||
expect(infoMessage[1] + ' ' + infoMessage[2]).toBe('beforeSave MyObject info log');
|
||||
return logController.getLogs({ from: Date.now() - 500, size: 1000 })
|
||||
}).then((res) => {
|
||||
expect(res.length).not.toBe(0);
|
||||
const lastLogs = res.slice(0, 3);
|
||||
const cloudTriggerMessage = lastLogs[0];
|
||||
const errorMessage = lastLogs[1];
|
||||
const infoMessage = lastLogs[2];
|
||||
expect(cloudTriggerMessage.level).toBe('info');
|
||||
expect(cloudTriggerMessage.triggerType).toEqual('beforeSave');
|
||||
expect(cloudTriggerMessage.message).toMatch(/beforeSave triggered for MyObject for user [^ ]*\n {2}Input: {}\n {2}Result: {}/);
|
||||
expect(cloudTriggerMessage.user).toBe(user.id);
|
||||
expect(errorMessage.level).toBe('error');
|
||||
expect(errorMessage.error).toBe('there was an error');
|
||||
expect(errorMessage.message).toBe('beforeSave MyObject error log');
|
||||
expect(infoMessage.level).toBe('info');
|
||||
expect(infoMessage.info).toBe('some log');
|
||||
expect(infoMessage.message).toBe('beforeSave MyObject info log');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -113,151 +113,117 @@ describe('Cloud Code Logger', () => {
|
||||
});
|
||||
|
||||
it('should truncate input and result of long lines', done => {
|
||||
const logController = new LoggerController(new WinstonLoggerAdapter());
|
||||
const longString = fs.readFileSync(loremFile, 'utf8');
|
||||
Parse.Cloud.define('aFunction', req => {
|
||||
return req.params;
|
||||
Parse.Cloud.define('aFunction', (req, res) => {
|
||||
res.success(req.params);
|
||||
});
|
||||
|
||||
Parse.Cloud.run('aFunction', { longString })
|
||||
.then(() => {
|
||||
const log = spy.calls.mostRecent().args;
|
||||
expect(log[0]).toEqual('info');
|
||||
expect(log[1]).toMatch(
|
||||
/Ran cloud function aFunction for user [^ ]* with:\n {2}Input: {.*?\(truncated\)$/m
|
||||
);
|
||||
done();
|
||||
})
|
||||
.then(null, e => done.fail(e));
|
||||
.then(() => logController.getLogs({ from: Date.now() - 500, size: 1000 }))
|
||||
.then(logs => {
|
||||
const log = logs[0];
|
||||
expect(log.level).toEqual('info');
|
||||
expect(log.message).toMatch(
|
||||
/Ran cloud function aFunction for user [^ ]* with:\n {2}Input: {.*?\(truncated\)$/m);
|
||||
done();
|
||||
})
|
||||
.then(null, e => done.fail(e));
|
||||
});
|
||||
|
||||
it('should log an afterSave', done => {
|
||||
Parse.Cloud.afterSave('MyObject', () => {});
|
||||
const logController = new LoggerController(new WinstonLoggerAdapter());
|
||||
Parse.Cloud.afterSave("MyObject", () => { });
|
||||
new Parse.Object('MyObject')
|
||||
.save()
|
||||
.then(() => {
|
||||
const log = spy.calls.mostRecent().args;
|
||||
expect(log[2].triggerType).toEqual('afterSave');
|
||||
done();
|
||||
})
|
||||
// catch errors - not that the error is actually useful :(
|
||||
.then(null, e => done.fail(e));
|
||||
.save()
|
||||
.then(() => logController.getLogs({ from: Date.now() - 500, size: 1000 }))
|
||||
.then((logs) => {
|
||||
const log = logs[0];
|
||||
expect(log.triggerType).toEqual('afterSave');
|
||||
done();
|
||||
})
|
||||
// catch errors - not that the error is actually useful :(
|
||||
.then(null, e => done.fail(e));
|
||||
});
|
||||
|
||||
it('should log a denied beforeSave', done => {
|
||||
Parse.Cloud.beforeSave('MyObject', () => {
|
||||
throw 'uh oh!';
|
||||
const logController = new LoggerController(new WinstonLoggerAdapter());
|
||||
Parse.Cloud.beforeSave("MyObject", (req, res) => {
|
||||
res.error('uh oh!');
|
||||
});
|
||||
|
||||
new Parse.Object('MyObject')
|
||||
.save()
|
||||
.then(
|
||||
() => done.fail('this is not supposed to succeed'),
|
||||
() => new Promise(resolve => setTimeout(resolve, 100))
|
||||
)
|
||||
.then(() => {
|
||||
const logs = spy.calls.all().reverse();
|
||||
const log = logs[1].args; // 0 is the 'uh oh!' from rejection...
|
||||
expect(log[0]).toEqual('error');
|
||||
const error = log[2].error;
|
||||
expect(error instanceof Parse.Error).toBeTruthy();
|
||||
expect(error.code).toBe(141);
|
||||
expect(error.message).toBe('uh oh!');
|
||||
done();
|
||||
});
|
||||
.save()
|
||||
.then(
|
||||
() => done.fail('this is not supposed to succeed'),
|
||||
() => logController.getLogs({ from: Date.now() - 500, size: 1000 })
|
||||
)
|
||||
.then(logs => {
|
||||
const log = logs[1]; // 0 is the 'uh oh!' from rejection...
|
||||
expect(log.level).toEqual('error');
|
||||
expect(log.error).toEqual({ code: 141, message: 'uh oh!' });
|
||||
done()
|
||||
});
|
||||
});
|
||||
|
||||
it('should log cloud function success', done => {
|
||||
Parse.Cloud.define('aFunction', () => {
|
||||
return 'it worked!';
|
||||
});
|
||||
const logController = new LoggerController(new WinstonLoggerAdapter());
|
||||
|
||||
Parse.Cloud.run('aFunction', { foo: 'bar' }).then(() => {
|
||||
const log = spy.calls.mostRecent().args;
|
||||
expect(log[0]).toEqual('info');
|
||||
expect(log[1]).toMatch(
|
||||
/Ran cloud function aFunction for user [^ ]* with:\n {2}Input: {"foo":"bar"}\n {2}Result: "it worked!/
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should log cloud function failure', done => {
|
||||
Parse.Cloud.define('aFunction', () => {
|
||||
throw 'it failed!';
|
||||
Parse.Cloud.define('aFunction', (req, res) => {
|
||||
res.success('it worked!');
|
||||
});
|
||||
|
||||
Parse.Cloud.run('aFunction', { foo: 'bar' })
|
||||
.catch(() => {})
|
||||
.then(() => {
|
||||
const logs = spy.calls.all().reverse();
|
||||
expect(logs[0].args[1]).toBe('Parse error: ');
|
||||
expect(logs[0].args[2].message).toBe('it failed!');
|
||||
.then(() => logController.getLogs({ from: Date.now() - 500, size: 1000 }))
|
||||
.then(logs => {
|
||||
const log = logs[0];
|
||||
expect(log.level).toEqual('info');
|
||||
expect(log.message).toMatch(
|
||||
/Ran cloud function aFunction for user [^ ]* with:\n {2}Input: {"foo":"bar"}\n {2}Result: "it worked!/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
const log = logs[1].args;
|
||||
expect(log[0]).toEqual('error');
|
||||
expect(log[1]).toMatch(
|
||||
/Failed running cloud function aFunction for user [^ ]* with:\n {2}Input: {"foo":"bar"}\n {2}Error:/
|
||||
);
|
||||
const errorString = JSON.stringify(new Parse.Error(141, 'it failed!'));
|
||||
expect(log[1].indexOf(errorString)).toBeGreaterThan(0);
|
||||
done();
|
||||
})
|
||||
.catch(done.fail);
|
||||
it('should log cloud function failure', done => {
|
||||
const logController = new LoggerController(new WinstonLoggerAdapter());
|
||||
|
||||
Parse.Cloud.define('aFunction', (req, res) => {
|
||||
res.error('it failed!');
|
||||
});
|
||||
|
||||
Parse.Cloud.run('aFunction', { foo: 'bar' })
|
||||
.then(null, () => logController.getLogs({ from: Date.now() - 500, size: 1000 }))
|
||||
.then(logs => {
|
||||
const log = logs[1];
|
||||
expect(log.level).toEqual('error');
|
||||
expect(log.message).toMatch(
|
||||
/Failed running cloud function aFunction for user [^ ]* with:\n {2}Input: {"foo":"bar"}\n {2}Error: {"code":141,"message":"it failed!"}/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
xit('should log a changed beforeSave indicating a change', done => {
|
||||
const logController = new LoggerController(new WinstonLoggerAdapter());
|
||||
|
||||
Parse.Cloud.beforeSave('MyObject', req => {
|
||||
Parse.Cloud.beforeSave("MyObject", (req, res) => {
|
||||
const myObj = req.object;
|
||||
myObj.set('aChange', true);
|
||||
return myObj;
|
||||
res.success(myObj);
|
||||
});
|
||||
|
||||
new Parse.Object('MyObject')
|
||||
.save()
|
||||
.then(() => logController.getLogs({ from: Date.now() - 500, size: 1000 }))
|
||||
.then(() => {
|
||||
// expect the log to indicate that it has changed
|
||||
/*
|
||||
.save()
|
||||
.then(() => logController.getLogs({ from: Date.now() - 500, size: 1000 }))
|
||||
.then(() => {
|
||||
// expect the log to indicate that it has changed
|
||||
/*
|
||||
Here's what it looks like on parse.com...
|
||||
|
||||
Input: {"original":{"clientVersion":"1","createdAt":"2016-06-02T05:29:08.694Z","image":{"__type":"File","name":"tfss-xxxxxxxx.png","url":"http://files.parsetfss.com/xxxxxxxx.png"},"lastScanDate":{"__type":"Date","iso":"2016-06-02T05:28:58.135Z"},"localIdentifier":"XXXXX","objectId":"OFHMX7ZUcI","status":... (truncated)
|
||||
Result: Update changed to {"object":{"__type":"Pointer","className":"Emoticode","objectId":"ksrq7z3Ehc"},"imageThumb":{"__type":"File","name":"tfss-xxxxxxx.png","url":"http://files.parsetfss.com/xxxxx.png"},"status":"success"}
|
||||
*/
|
||||
done();
|
||||
})
|
||||
.then(null, e => done.fail(JSON.stringify(e)));
|
||||
done();
|
||||
})
|
||||
.then(null, e => done.fail(JSON.stringify(e)));
|
||||
}).pend('needs more work.....');
|
||||
|
||||
it('cloud function should obfuscate password', done => {
|
||||
Parse.Cloud.define('testFunction', () => {
|
||||
return 'verify code success';
|
||||
});
|
||||
|
||||
Parse.Cloud.run('testFunction', { username: 'hawk', password: '123456' })
|
||||
.then(() => {
|
||||
const entry = spy.calls.mostRecent().args;
|
||||
expect(entry[2].params.password).toMatch(/\*\*\*\*\*\*\*\*/);
|
||||
done();
|
||||
})
|
||||
.then(null, e => done.fail(e));
|
||||
});
|
||||
|
||||
it('should only log once for object not found', async () => {
|
||||
const config = Config.get('test');
|
||||
const spy = spyOn(config.loggerController, 'error').and.callThrough();
|
||||
try {
|
||||
const object = new Parse.Object('Object');
|
||||
object.id = 'invalid';
|
||||
await object.fetch();
|
||||
} catch (e) {
|
||||
/**/
|
||||
}
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(spy.calls.count()).toBe(1);
|
||||
const { args } = spy.calls.mostRecent();
|
||||
expect(args[0]).toBe('Parse error: ');
|
||||
expect(args[1].message).toBe('Object not found.');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,296 +0,0 @@
|
||||
const DatabaseController = require('../lib/Controllers/DatabaseController.js');
|
||||
const validateQuery = DatabaseController._validateQuery;
|
||||
|
||||
describe('DatabaseController', function () {
|
||||
describe('validateQuery', function () {
|
||||
it('should not restructure simple cases of SERVER-13732', done => {
|
||||
const query = {
|
||||
$or: [{ a: 1 }, { a: 2 }],
|
||||
_rperm: { $in: ['a', 'b'] },
|
||||
foo: 3,
|
||||
};
|
||||
validateQuery(query);
|
||||
expect(query).toEqual({
|
||||
$or: [{ a: 1 }, { a: 2 }],
|
||||
_rperm: { $in: ['a', 'b'] },
|
||||
foo: 3,
|
||||
});
|
||||
done();
|
||||
});
|
||||
|
||||
it('should not restructure SERVER-13732 queries with $nears', done => {
|
||||
let query = { $or: [{ a: 1 }, { b: 1 }], c: { $nearSphere: {} } };
|
||||
validateQuery(query);
|
||||
expect(query).toEqual({
|
||||
$or: [{ a: 1 }, { b: 1 }],
|
||||
c: { $nearSphere: {} },
|
||||
});
|
||||
query = { $or: [{ a: 1 }, { b: 1 }], c: { $near: {} } };
|
||||
validateQuery(query);
|
||||
expect(query).toEqual({ $or: [{ a: 1 }, { b: 1 }], c: { $near: {} } });
|
||||
done();
|
||||
});
|
||||
|
||||
it('should not push refactored keys down a tree for SERVER-13732', done => {
|
||||
const query = {
|
||||
a: 1,
|
||||
$or: [{ $or: [{ b: 1 }, { b: 2 }] }, { $or: [{ c: 1 }, { c: 2 }] }],
|
||||
};
|
||||
validateQuery(query);
|
||||
expect(query).toEqual({
|
||||
a: 1,
|
||||
$or: [{ $or: [{ b: 1 }, { b: 2 }] }, { $or: [{ c: 1 }, { c: 2 }] }],
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should reject invalid queries', done => {
|
||||
expect(() => validateQuery({ $or: { a: 1 } })).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('should accept valid queries', done => {
|
||||
expect(() => validateQuery({ $or: [{ a: 1 }, { b: 2 }] })).not.toThrow();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('addPointerPermissions', function () {
|
||||
const CLASS_NAME = 'Foo';
|
||||
const USER_ID = 'userId';
|
||||
const ACL_GROUP = [USER_ID];
|
||||
const OPERATION = 'find';
|
||||
|
||||
const databaseController = new DatabaseController();
|
||||
const schemaController = jasmine.createSpyObj('SchemaController', [
|
||||
'testPermissionsForClassName',
|
||||
'getClassLevelPermissions',
|
||||
'getExpectedType',
|
||||
]);
|
||||
|
||||
it('should not decorate query if no pointer CLPs are present', done => {
|
||||
const clp = buildCLP();
|
||||
const query = { a: 'b' };
|
||||
|
||||
schemaController.testPermissionsForClassName
|
||||
.withArgs(CLASS_NAME, ACL_GROUP, OPERATION)
|
||||
.and.returnValue(true);
|
||||
schemaController.getClassLevelPermissions.withArgs(CLASS_NAME).and.returnValue(clp);
|
||||
|
||||
const output = databaseController.addPointerPermissions(
|
||||
schemaController,
|
||||
CLASS_NAME,
|
||||
OPERATION,
|
||||
query,
|
||||
ACL_GROUP
|
||||
);
|
||||
|
||||
expect(output).toEqual({ ...query });
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should decorate query if a pointer CLP entry is present', done => {
|
||||
const clp = buildCLP(['user']);
|
||||
const query = { a: 'b' };
|
||||
|
||||
schemaController.testPermissionsForClassName
|
||||
.withArgs(CLASS_NAME, ACL_GROUP, OPERATION)
|
||||
.and.returnValue(false);
|
||||
schemaController.getClassLevelPermissions.withArgs(CLASS_NAME).and.returnValue(clp);
|
||||
schemaController.getExpectedType
|
||||
.withArgs(CLASS_NAME, 'user')
|
||||
.and.returnValue({ type: 'Pointer' });
|
||||
|
||||
const output = databaseController.addPointerPermissions(
|
||||
schemaController,
|
||||
CLASS_NAME,
|
||||
OPERATION,
|
||||
query,
|
||||
ACL_GROUP
|
||||
);
|
||||
|
||||
expect(output).toEqual({ ...query, user: createUserPointer(USER_ID) });
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should decorate query if an array CLP entry is present', done => {
|
||||
const clp = buildCLP(['users']);
|
||||
const query = { a: 'b' };
|
||||
|
||||
schemaController.testPermissionsForClassName
|
||||
.withArgs(CLASS_NAME, ACL_GROUP, OPERATION)
|
||||
.and.returnValue(false);
|
||||
schemaController.getClassLevelPermissions.withArgs(CLASS_NAME).and.returnValue(clp);
|
||||
schemaController.getExpectedType
|
||||
.withArgs(CLASS_NAME, 'users')
|
||||
.and.returnValue({ type: 'Array' });
|
||||
|
||||
const output = databaseController.addPointerPermissions(
|
||||
schemaController,
|
||||
CLASS_NAME,
|
||||
OPERATION,
|
||||
query,
|
||||
ACL_GROUP
|
||||
);
|
||||
|
||||
expect(output).toEqual({
|
||||
...query,
|
||||
users: { $all: [createUserPointer(USER_ID)] },
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should decorate query if an object CLP entry is present', done => {
|
||||
const clp = buildCLP(['user']);
|
||||
const query = { a: 'b' };
|
||||
|
||||
schemaController.testPermissionsForClassName
|
||||
.withArgs(CLASS_NAME, ACL_GROUP, OPERATION)
|
||||
.and.returnValue(false);
|
||||
schemaController.getClassLevelPermissions.withArgs(CLASS_NAME).and.returnValue(clp);
|
||||
schemaController.getExpectedType
|
||||
.withArgs(CLASS_NAME, 'user')
|
||||
.and.returnValue({ type: 'Object' });
|
||||
|
||||
const output = databaseController.addPointerPermissions(
|
||||
schemaController,
|
||||
CLASS_NAME,
|
||||
OPERATION,
|
||||
query,
|
||||
ACL_GROUP
|
||||
);
|
||||
|
||||
expect(output).toEqual({
|
||||
...query,
|
||||
user: createUserPointer(USER_ID),
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should decorate query if a pointer CLP is present and the same field is part of the query', done => {
|
||||
const clp = buildCLP(['user']);
|
||||
const query = { a: 'b', user: 'a' };
|
||||
|
||||
schemaController.testPermissionsForClassName
|
||||
.withArgs(CLASS_NAME, ACL_GROUP, OPERATION)
|
||||
.and.returnValue(false);
|
||||
schemaController.getClassLevelPermissions.withArgs(CLASS_NAME).and.returnValue(clp);
|
||||
schemaController.getExpectedType
|
||||
.withArgs(CLASS_NAME, 'user')
|
||||
.and.returnValue({ type: 'Pointer' });
|
||||
|
||||
const output = databaseController.addPointerPermissions(
|
||||
schemaController,
|
||||
CLASS_NAME,
|
||||
OPERATION,
|
||||
query,
|
||||
ACL_GROUP
|
||||
);
|
||||
|
||||
expect(output).toEqual({
|
||||
$and: [{ user: createUserPointer(USER_ID) }, { ...query }],
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should transform the query to an $or query if multiple array/pointer CLPs are present', done => {
|
||||
const clp = buildCLP(['user', 'users', 'userObject']);
|
||||
const query = { a: 'b' };
|
||||
|
||||
schemaController.testPermissionsForClassName
|
||||
.withArgs(CLASS_NAME, ACL_GROUP, OPERATION)
|
||||
.and.returnValue(false);
|
||||
schemaController.getClassLevelPermissions.withArgs(CLASS_NAME).and.returnValue(clp);
|
||||
schemaController.getExpectedType
|
||||
.withArgs(CLASS_NAME, 'user')
|
||||
.and.returnValue({ type: 'Pointer' });
|
||||
schemaController.getExpectedType
|
||||
.withArgs(CLASS_NAME, 'users')
|
||||
.and.returnValue({ type: 'Array' });
|
||||
schemaController.getExpectedType
|
||||
.withArgs(CLASS_NAME, 'userObject')
|
||||
.and.returnValue({ type: 'Object' });
|
||||
|
||||
const output = databaseController.addPointerPermissions(
|
||||
schemaController,
|
||||
CLASS_NAME,
|
||||
OPERATION,
|
||||
query,
|
||||
ACL_GROUP
|
||||
);
|
||||
|
||||
expect(output).toEqual({
|
||||
$or: [
|
||||
{ ...query, user: createUserPointer(USER_ID) },
|
||||
{ ...query, users: { $all: [createUserPointer(USER_ID)] } },
|
||||
{ ...query, userObject: createUserPointer(USER_ID) },
|
||||
],
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should throw an error if for some unexpected reason the property specified in the CLP is neither a pointer nor an array', done => {
|
||||
const clp = buildCLP(['user']);
|
||||
const query = { a: 'b' };
|
||||
|
||||
schemaController.testPermissionsForClassName
|
||||
.withArgs(CLASS_NAME, ACL_GROUP, OPERATION)
|
||||
.and.returnValue(false);
|
||||
schemaController.getClassLevelPermissions.withArgs(CLASS_NAME).and.returnValue(clp);
|
||||
schemaController.getExpectedType
|
||||
.withArgs(CLASS_NAME, 'user')
|
||||
.and.returnValue({ type: 'Number' });
|
||||
|
||||
expect(() => {
|
||||
databaseController.addPointerPermissions(
|
||||
schemaController,
|
||||
CLASS_NAME,
|
||||
OPERATION,
|
||||
query,
|
||||
ACL_GROUP
|
||||
);
|
||||
}).toThrow(
|
||||
Error(
|
||||
`An unexpected condition occurred when resolving pointer permissions: ${CLASS_NAME} user`
|
||||
)
|
||||
);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function buildCLP(pointerNames) {
|
||||
const OPERATIONS = ['count', 'find', 'get', 'create', 'update', 'delete', 'addField'];
|
||||
|
||||
const clp = OPERATIONS.reduce((acc, op) => {
|
||||
acc[op] = {};
|
||||
|
||||
if (pointerNames && pointerNames.length) {
|
||||
acc[op].pointerFields = pointerNames;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
clp.protectedFields = {};
|
||||
clp.writeUserFields = [];
|
||||
clp.readUserFields = [];
|
||||
|
||||
return clp;
|
||||
}
|
||||
|
||||
function createUserPointer(userId) {
|
||||
return {
|
||||
__type: 'Pointer',
|
||||
className: '_User',
|
||||
objectId: userId,
|
||||
};
|
||||
}
|
||||
+345
-830
File diff suppressed because it is too large
Load Diff
@@ -1,33 +0,0 @@
|
||||
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 => {
|
||||
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);
|
||||
});
|
||||
|
||||
try {
|
||||
await request({
|
||||
method: 'PUT',
|
||||
url: defaultConfiguration.serverURL + '/classes/AnyClass/nonExistingId',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': defaultConfiguration.appId,
|
||||
'X-Parse-Master-Key': defaultConfiguration.masterKey,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: { someField: 'blablabla' },
|
||||
});
|
||||
fail('Should throw error');
|
||||
} catch (response) {
|
||||
expect(response).toBeDefined();
|
||||
expect(response.status).toEqual(500);
|
||||
parseServer.server.close(done);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,58 +1,49 @@
|
||||
const auth = require('../lib/Auth');
|
||||
const Config = require('../lib/Config');
|
||||
const rest = require('../lib/rest');
|
||||
const auth = require('../src/Auth');
|
||||
const Config = require('../src/Config');
|
||||
const rest = require('../src/rest');
|
||||
|
||||
describe('Enable single schema cache', () => {
|
||||
beforeEach(done => {
|
||||
beforeEach((done) => {
|
||||
reconfigureServer({
|
||||
enableSingleSchemaCache: true,
|
||||
schemaCacheTTL: 30000,
|
||||
schemaCacheTTL: 30000
|
||||
}).then(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can perform multiple create and query operations', done => {
|
||||
it('can perform multiple create and query operations', (done) => {
|
||||
let config = fakeRequestForConfig();
|
||||
let nobody = auth.nobody(config);
|
||||
rest
|
||||
.create(config, nobody, 'Foo', { type: 1 })
|
||||
.then(() => {
|
||||
config = fakeRequestForConfig();
|
||||
nobody = auth.nobody(config);
|
||||
return rest.create(config, nobody, 'Foo', { type: 2 });
|
||||
})
|
||||
.then(() => {
|
||||
config = fakeRequestForConfig();
|
||||
nobody = auth.nobody(config);
|
||||
return rest.create(config, nobody, 'Bar');
|
||||
})
|
||||
.then(() => {
|
||||
config = fakeRequestForConfig();
|
||||
nobody = auth.nobody(config);
|
||||
return rest.find(config, nobody, 'Bar', { type: 1 });
|
||||
})
|
||||
.then(
|
||||
() => {
|
||||
fail('Should throw error');
|
||||
done();
|
||||
},
|
||||
error => {
|
||||
config = fakeRequestForConfig();
|
||||
nobody = auth.nobody(config);
|
||||
expect(error).toBeDefined();
|
||||
return rest.find(config, nobody, 'Foo', { type: 1 });
|
||||
}
|
||||
)
|
||||
.then(response => {
|
||||
config = fakeRequestForConfig();
|
||||
nobody = auth.nobody(config);
|
||||
expect(response.results.length).toEqual(1);
|
||||
done();
|
||||
});
|
||||
rest.create(config, nobody, 'Foo', {type: 1}).then(() => {
|
||||
config = fakeRequestForConfig();
|
||||
nobody = auth.nobody(config);
|
||||
return rest.create(config, nobody, 'Foo', {type: 2});
|
||||
}).then(() => {
|
||||
config = fakeRequestForConfig();
|
||||
nobody = auth.nobody(config);
|
||||
return rest.create(config, nobody, 'Bar');
|
||||
}).then(() => {
|
||||
config = fakeRequestForConfig();
|
||||
nobody = auth.nobody(config);
|
||||
return rest.find(config, nobody, 'Bar', {type: 1});
|
||||
}).then(() => {
|
||||
fail('Should throw error');
|
||||
done();
|
||||
}, (error) => {
|
||||
config = fakeRequestForConfig();
|
||||
nobody = auth.nobody(config);
|
||||
expect(error).toBeDefined();
|
||||
return rest.find(config, nobody, 'Foo', {type: 1});
|
||||
}).then((response) => {
|
||||
config = fakeRequestForConfig();
|
||||
nobody = auth.nobody(config);
|
||||
expect(response.results.length).toEqual(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const fakeRequestForConfig = function () {
|
||||
return Config.get('test');
|
||||
const fakeRequestForConfig = function() {
|
||||
return new Config('test');
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
const EventEmitterPubSub = require('../lib/Adapters/PubSub/EventEmitterPubSub').EventEmitterPubSub;
|
||||
var EventEmitterPubSub = require('../src/Adapters/PubSub/EventEmitterPubSub').EventEmitterPubSub;
|
||||
|
||||
describe('EventEmitterPubSub', function () {
|
||||
it('can publish and subscribe', function () {
|
||||
const publisher = EventEmitterPubSub.createPublisher();
|
||||
const subscriber = EventEmitterPubSub.createSubscriber();
|
||||
describe('EventEmitterPubSub', function() {
|
||||
it('can publish and subscribe', function() {
|
||||
var publisher = EventEmitterPubSub.createPublisher();
|
||||
var subscriber = EventEmitterPubSub.createSubscriber();
|
||||
subscriber.subscribe('testChannel');
|
||||
// Register mock checked for subscriber
|
||||
let isChecked = false;
|
||||
subscriber.on('message', function (channel, message) {
|
||||
var isChecked = false;
|
||||
subscriber.on('message', function(channel, message) {
|
||||
isChecked = true;
|
||||
expect(channel).toBe('testChannel');
|
||||
expect(message).toBe('testMessage');
|
||||
@@ -18,14 +18,14 @@ describe('EventEmitterPubSub', function () {
|
||||
expect(isChecked).toBe(true);
|
||||
});
|
||||
|
||||
it('can unsubscribe', function () {
|
||||
const publisher = EventEmitterPubSub.createPublisher();
|
||||
const subscriber = EventEmitterPubSub.createSubscriber();
|
||||
it('can unsubscribe', function() {
|
||||
var publisher = EventEmitterPubSub.createPublisher();
|
||||
var subscriber = EventEmitterPubSub.createSubscriber();
|
||||
subscriber.subscribe('testChannel');
|
||||
subscriber.unsubscribe('testChannel');
|
||||
// Register mock checked for subscriber
|
||||
let isCalled = false;
|
||||
subscriber.on('message', function () {
|
||||
var isCalled = false;
|
||||
subscriber.on('message', function() {
|
||||
isCalled = true;
|
||||
});
|
||||
|
||||
@@ -34,8 +34,8 @@ describe('EventEmitterPubSub', function () {
|
||||
expect(isCalled).toBe(false);
|
||||
});
|
||||
|
||||
it('can unsubscribe not subscribing channel', function () {
|
||||
const subscriber = EventEmitterPubSub.createSubscriber();
|
||||
it('can unsubscribe not subscribing channel', function() {
|
||||
var subscriber = EventEmitterPubSub.createSubscriber();
|
||||
|
||||
// Make sure subscriber does not throw exception
|
||||
subscriber.unsubscribe('testChannel');
|
||||
|
||||
+18
-135
@@ -1,147 +1,30 @@
|
||||
const LoggerController = require('../lib/Controllers/LoggerController').LoggerController;
|
||||
const WinstonLoggerAdapter = require('../lib/Adapters/Logger/WinstonLoggerAdapter')
|
||||
.WinstonLoggerAdapter;
|
||||
const GridFSBucketAdapter = require('../lib/Adapters/Files/GridFSBucketAdapter')
|
||||
.GridFSBucketAdapter;
|
||||
const GridStoreAdapter = require('../lib/Adapters/Files/GridStoreAdapter').GridStoreAdapter;
|
||||
const Config = require('../lib/Config');
|
||||
const FilesController = require('../lib/Controllers/FilesController').default;
|
||||
const databaseURI = 'mongodb://localhost:27017/parse';
|
||||
var GridStoreAdapter = require("../src/Adapters/Files/GridStoreAdapter").GridStoreAdapter;
|
||||
var Config = require("../src/Config");
|
||||
var FilesController = require('../src/Controllers/FilesController').default;
|
||||
|
||||
const mockAdapter = {
|
||||
createFile: () => {
|
||||
return Promise.reject(new Error('it failed with xyz'));
|
||||
},
|
||||
deleteFile: () => {},
|
||||
getFileData: () => {},
|
||||
getFileLocation: () => 'xyz',
|
||||
validateFilename: () => {
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
// Small additional tests to improve overall coverage
|
||||
describe('FilesController', () => {
|
||||
it('should properly expand objects', done => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const gridStoreAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
const filesController = new FilesController(gridStoreAdapter);
|
||||
const result = filesController.expandFilesInObject(config, function () {});
|
||||
describe("FilesController",() =>{
|
||||
it("should properly expand objects", (done) => {
|
||||
|
||||
var config = new Config(Parse.applicationId);
|
||||
var gridStoreAdapter = new GridStoreAdapter('mongodb://localhost:27017/parse');
|
||||
var filesController = new FilesController(gridStoreAdapter)
|
||||
var result = filesController.expandFilesInObject(config, function(){});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
|
||||
const fullFile = {
|
||||
var fullFile = {
|
||||
type: '__type',
|
||||
url: 'http://an.url',
|
||||
};
|
||||
url: "http://an.url"
|
||||
}
|
||||
|
||||
const anObject = {
|
||||
aFile: fullFile,
|
||||
};
|
||||
var anObject = {
|
||||
aFile: fullFile
|
||||
}
|
||||
filesController.expandFilesInObject(config, anObject);
|
||||
expect(anObject.aFile.url).toEqual('http://an.url');
|
||||
expect(anObject.aFile.url).toEqual("http://an.url");
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should create a server log on failure', done => {
|
||||
const logController = new LoggerController(new WinstonLoggerAdapter());
|
||||
|
||||
reconfigureServer({ filesAdapter: mockAdapter })
|
||||
.then(() => new Parse.File('yolo.txt', [1, 2, 3], 'text/plain').save())
|
||||
.then(
|
||||
() => done.fail('should not succeed'),
|
||||
() => setImmediate(() => Promise.resolve('done'))
|
||||
)
|
||||
.then(() => new Promise(resolve => setTimeout(resolve, 200)))
|
||||
.then(() => logController.getLogs({ from: Date.now() - 1000, size: 1000 }))
|
||||
.then(logs => {
|
||||
// we get two logs here: 1. the source of the failure to save the file
|
||||
// and 2 the message that will be sent back to the client.
|
||||
|
||||
const log1 = logs.find(x => x.message === 'Error creating a file: it failed with xyz');
|
||||
expect(log1.level).toBe('error');
|
||||
|
||||
const log2 = logs.find(x => x.message === 'it failed with xyz');
|
||||
expect(log2.level).toBe('error');
|
||||
expect(log2.code).toBe(130);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a parse error when a string is returned', done => {
|
||||
const mock2 = mockAdapter;
|
||||
mock2.validateFilename = () => {
|
||||
return 'Bad file! No biscuit!';
|
||||
};
|
||||
const filesController = new FilesController(mockAdapter);
|
||||
const error = filesController.validateFilename();
|
||||
expect(typeof error).toBe('object');
|
||||
expect(error.message.indexOf('biscuit')).toBe(13);
|
||||
expect(error.code).toBe(Parse.Error.INVALID_FILE_NAME);
|
||||
done();
|
||||
});
|
||||
|
||||
it('should add a unique hash to the file name when the preserveFileName option is false', done => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const gridStoreAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
spyOn(gridStoreAdapter, 'createFile');
|
||||
gridStoreAdapter.createFile.and.returnValue(Promise.resolve());
|
||||
const fileName = 'randomFileName.pdf';
|
||||
const regexEscapedFileName = fileName.replace(/\./g, '\\$&');
|
||||
const filesController = new FilesController(gridStoreAdapter, null, {
|
||||
preserveFileName: false,
|
||||
});
|
||||
|
||||
filesController.createFile(config, fileName);
|
||||
|
||||
expect(gridStoreAdapter.createFile).toHaveBeenCalledTimes(1);
|
||||
expect(gridStoreAdapter.createFile.calls.mostRecent().args[0]).toMatch(
|
||||
`^.{32}_${regexEscapedFileName}$`
|
||||
);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should not add a unique hash to the file name when the preserveFileName option is true', done => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const gridStoreAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
spyOn(gridStoreAdapter, 'createFile');
|
||||
gridStoreAdapter.createFile.and.returnValue(Promise.resolve());
|
||||
const fileName = 'randomFileName.pdf';
|
||||
const filesController = new FilesController(gridStoreAdapter, null, {
|
||||
preserveFileName: true,
|
||||
});
|
||||
|
||||
filesController.createFile(config, fileName);
|
||||
|
||||
expect(gridStoreAdapter.createFile).toHaveBeenCalledTimes(1);
|
||||
expect(gridStoreAdapter.createFile.calls.mostRecent().args[0]).toEqual(fileName);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should handle adapter without getMetadata', async () => {
|
||||
const gridStoreAdapter = new GridFSBucketAdapter(databaseURI);
|
||||
gridStoreAdapter.getMetadata = null;
|
||||
const filesController = new FilesController(gridStoreAdapter);
|
||||
|
||||
const result = await filesController.getMetadata();
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should reject slashes in file names', done => {
|
||||
const gridStoreAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
const fileName = 'foo/randomFileName.pdf';
|
||||
expect(gridStoreAdapter.validateFilename(fileName)).not.toBe(null);
|
||||
done();
|
||||
});
|
||||
|
||||
it('should also reject slashes in file names', done => {
|
||||
const gridStoreAdapter = new GridStoreAdapter('mongodb://localhost:27017/parse');
|
||||
const fileName = 'foo/randomFileName.pdf';
|
||||
expect(gridStoreAdapter.validateFilename(fileName)).not.toBe(null);
|
||||
done();
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1,457 +0,0 @@
|
||||
const GridStoreAdapter = require('../lib/Adapters/Files/GridStoreAdapter').GridStoreAdapter;
|
||||
const GridFSBucketAdapter = require('../lib/Adapters/Files/GridFSBucketAdapter')
|
||||
.GridFSBucketAdapter;
|
||||
const { randomString } = require('../lib/cryptoUtils');
|
||||
const databaseURI = 'mongodb://localhost:27017/parse';
|
||||
const request = require('../lib/request');
|
||||
const Config = require('../lib/Config');
|
||||
|
||||
async function expectMissingFile(gfsAdapter, name) {
|
||||
try {
|
||||
await gfsAdapter.getFileData(name);
|
||||
fail('should have thrown');
|
||||
} catch (e) {
|
||||
expect(e.message).toEqual('FileNotFound: file myFileName was not found');
|
||||
}
|
||||
}
|
||||
|
||||
describe_only_db('mongo')('GridFSBucket and GridStore interop', () => {
|
||||
beforeEach(async () => {
|
||||
const gsAdapter = new GridStoreAdapter(databaseURI);
|
||||
const db = await gsAdapter._connect();
|
||||
await db.dropDatabase();
|
||||
});
|
||||
|
||||
it('a file created in GridStore should be available in GridFS', async () => {
|
||||
const gsAdapter = new GridStoreAdapter(databaseURI);
|
||||
const gfsAdapter = new GridFSBucketAdapter(databaseURI);
|
||||
await expectMissingFile(gfsAdapter, 'myFileName');
|
||||
const originalString = 'abcdefghi';
|
||||
await gsAdapter.createFile('myFileName', originalString);
|
||||
const gsResult = await gsAdapter.getFileData('myFileName');
|
||||
expect(gsResult.toString('utf8')).toBe(originalString);
|
||||
const gfsResult = await gfsAdapter.getFileData('myFileName');
|
||||
expect(gfsResult.toString('utf8')).toBe(originalString);
|
||||
});
|
||||
|
||||
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(
|
||||
databaseURI,
|
||||
{},
|
||||
'89E4AFF1-DFE4-4603-9574-BFA16BB446FD'
|
||||
);
|
||||
await expectMissingFile(encryptedAdapter, 'myFileName');
|
||||
const originalString = 'abcdefghi';
|
||||
await encryptedAdapter.createFile('myFileName', originalString);
|
||||
const unencryptedResult = await unencryptedAdapter.getFileData('myFileName');
|
||||
expect(unencryptedResult.toString('utf8')).not.toBe(originalString);
|
||||
const encryptedResult = await encryptedAdapter.getFileData('myFileName');
|
||||
expect(encryptedResult.toString('utf8')).toBe(originalString);
|
||||
});
|
||||
|
||||
it('should rotate key of all unencrypted GridFS files to encrypted files', async () => {
|
||||
const unencryptedAdapter = new GridFSBucketAdapter(databaseURI);
|
||||
const encryptedAdapter = new GridFSBucketAdapter(
|
||||
databaseURI,
|
||||
{},
|
||||
'89E4AFF1-DFE4-4603-9574-BFA16BB446FD'
|
||||
);
|
||||
const fileName1 = 'file1.txt';
|
||||
const data1 = 'hello world';
|
||||
const fileName2 = 'file2.txt';
|
||||
const data2 = 'hello new world';
|
||||
//Store unecrypted files
|
||||
await unencryptedAdapter.createFile(fileName1, data1);
|
||||
const unencryptedResult1 = await unencryptedAdapter.getFileData(fileName1);
|
||||
expect(unencryptedResult1.toString('utf8')).toBe(data1);
|
||||
await unencryptedAdapter.createFile(fileName2, data2);
|
||||
const unencryptedResult2 = await unencryptedAdapter.getFileData(fileName2);
|
||||
expect(unencryptedResult2.toString('utf8')).toBe(data2);
|
||||
//Check if encrypted adapter can read data and make sure it's not the same as unEncrypted adapter
|
||||
const { rotated, notRotated } = await encryptedAdapter.rotateEncryptionKey();
|
||||
expect(rotated.length).toEqual(2);
|
||||
expect(
|
||||
rotated.filter(function (value) {
|
||||
return value === fileName1;
|
||||
}).length
|
||||
).toEqual(1);
|
||||
expect(
|
||||
rotated.filter(function (value) {
|
||||
return value === fileName2;
|
||||
}).length
|
||||
).toEqual(1);
|
||||
expect(notRotated.length).toEqual(0);
|
||||
let result = await encryptedAdapter.getFileData(fileName1);
|
||||
expect(result instanceof Buffer).toBe(true);
|
||||
expect(result.toString('utf-8')).toEqual(data1);
|
||||
const encryptedData1 = await unencryptedAdapter.getFileData(fileName1);
|
||||
expect(encryptedData1.toString('utf-8')).not.toEqual(unencryptedResult1);
|
||||
result = await encryptedAdapter.getFileData(fileName2);
|
||||
expect(result instanceof Buffer).toBe(true);
|
||||
expect(result.toString('utf-8')).toEqual(data2);
|
||||
const encryptedData2 = await unencryptedAdapter.getFileData(fileName2);
|
||||
expect(encryptedData2.toString('utf-8')).not.toEqual(unencryptedResult2);
|
||||
});
|
||||
|
||||
it('should rotate key of all old encrypted GridFS files to encrypted files', async () => {
|
||||
const oldEncryptionKey = 'oldKeyThatILoved';
|
||||
const oldEncryptedAdapter = new GridFSBucketAdapter(databaseURI, {}, oldEncryptionKey);
|
||||
const encryptedAdapter = new GridFSBucketAdapter(databaseURI, {}, 'newKeyThatILove');
|
||||
const fileName1 = 'file1.txt';
|
||||
const data1 = 'hello world';
|
||||
const fileName2 = 'file2.txt';
|
||||
const data2 = 'hello new world';
|
||||
//Store unecrypted files
|
||||
await oldEncryptedAdapter.createFile(fileName1, data1);
|
||||
const oldEncryptedResult1 = await oldEncryptedAdapter.getFileData(fileName1);
|
||||
expect(oldEncryptedResult1.toString('utf8')).toBe(data1);
|
||||
await oldEncryptedAdapter.createFile(fileName2, data2);
|
||||
const oldEncryptedResult2 = await oldEncryptedAdapter.getFileData(fileName2);
|
||||
expect(oldEncryptedResult2.toString('utf8')).toBe(data2);
|
||||
//Check if encrypted adapter can read data and make sure it's not the same as unEncrypted adapter
|
||||
const { rotated, notRotated } = await encryptedAdapter.rotateEncryptionKey({
|
||||
oldKey: oldEncryptionKey,
|
||||
});
|
||||
expect(rotated.length).toEqual(2);
|
||||
expect(
|
||||
rotated.filter(function (value) {
|
||||
return value === fileName1;
|
||||
}).length
|
||||
).toEqual(1);
|
||||
expect(
|
||||
rotated.filter(function (value) {
|
||||
return value === fileName2;
|
||||
}).length
|
||||
).toEqual(1);
|
||||
expect(notRotated.length).toEqual(0);
|
||||
let result = await encryptedAdapter.getFileData(fileName1);
|
||||
expect(result instanceof Buffer).toBe(true);
|
||||
expect(result.toString('utf-8')).toEqual(data1);
|
||||
let decryptionError1;
|
||||
let encryptedData1;
|
||||
try {
|
||||
encryptedData1 = await oldEncryptedAdapter.getFileData(fileName1);
|
||||
} catch (err) {
|
||||
decryptionError1 = err;
|
||||
}
|
||||
expect(decryptionError1).toMatch('Error');
|
||||
expect(encryptedData1).toBeUndefined();
|
||||
result = await encryptedAdapter.getFileData(fileName2);
|
||||
expect(result instanceof Buffer).toBe(true);
|
||||
expect(result.toString('utf-8')).toEqual(data2);
|
||||
let decryptionError2;
|
||||
let encryptedData2;
|
||||
try {
|
||||
encryptedData2 = await oldEncryptedAdapter.getFileData(fileName2);
|
||||
} catch (err) {
|
||||
decryptionError2 = err;
|
||||
}
|
||||
expect(decryptionError2).toMatch('Error');
|
||||
expect(encryptedData2).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should rotate key of all old encrypted GridFS files to unencrypted files', async () => {
|
||||
const oldEncryptionKey = 'oldKeyThatILoved';
|
||||
const oldEncryptedAdapter = new GridFSBucketAdapter(databaseURI, {}, oldEncryptionKey);
|
||||
const unEncryptedAdapter = new GridFSBucketAdapter(databaseURI);
|
||||
const fileName1 = 'file1.txt';
|
||||
const data1 = 'hello world';
|
||||
const fileName2 = 'file2.txt';
|
||||
const data2 = 'hello new world';
|
||||
//Store unecrypted files
|
||||
await oldEncryptedAdapter.createFile(fileName1, data1);
|
||||
const oldEncryptedResult1 = await oldEncryptedAdapter.getFileData(fileName1);
|
||||
expect(oldEncryptedResult1.toString('utf8')).toBe(data1);
|
||||
await oldEncryptedAdapter.createFile(fileName2, data2);
|
||||
const oldEncryptedResult2 = await oldEncryptedAdapter.getFileData(fileName2);
|
||||
expect(oldEncryptedResult2.toString('utf8')).toBe(data2);
|
||||
//Check if unEncrypted adapter can read data and make sure it's not the same as oldEncrypted adapter
|
||||
const { rotated, notRotated } = await unEncryptedAdapter.rotateEncryptionKey({
|
||||
oldKey: oldEncryptionKey,
|
||||
});
|
||||
expect(rotated.length).toEqual(2);
|
||||
expect(
|
||||
rotated.filter(function (value) {
|
||||
return value === fileName1;
|
||||
}).length
|
||||
).toEqual(1);
|
||||
expect(
|
||||
rotated.filter(function (value) {
|
||||
return value === fileName2;
|
||||
}).length
|
||||
).toEqual(1);
|
||||
expect(notRotated.length).toEqual(0);
|
||||
let result = await unEncryptedAdapter.getFileData(fileName1);
|
||||
expect(result instanceof Buffer).toBe(true);
|
||||
expect(result.toString('utf-8')).toEqual(data1);
|
||||
let decryptionError1;
|
||||
let encryptedData1;
|
||||
try {
|
||||
encryptedData1 = await oldEncryptedAdapter.getFileData(fileName1);
|
||||
} catch (err) {
|
||||
decryptionError1 = err;
|
||||
}
|
||||
expect(decryptionError1).toMatch('Error');
|
||||
expect(encryptedData1).toBeUndefined();
|
||||
result = await unEncryptedAdapter.getFileData(fileName2);
|
||||
expect(result instanceof Buffer).toBe(true);
|
||||
expect(result.toString('utf-8')).toEqual(data2);
|
||||
let decryptionError2;
|
||||
let encryptedData2;
|
||||
try {
|
||||
encryptedData2 = await oldEncryptedAdapter.getFileData(fileName2);
|
||||
} catch (err) {
|
||||
decryptionError2 = err;
|
||||
}
|
||||
expect(decryptionError2).toMatch('Error');
|
||||
expect(encryptedData2).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should only encrypt specified fileNames', async () => {
|
||||
const oldEncryptionKey = 'oldKeyThatILoved';
|
||||
const oldEncryptedAdapter = new GridFSBucketAdapter(databaseURI, {}, oldEncryptionKey);
|
||||
const encryptedAdapter = new GridFSBucketAdapter(databaseURI, {}, 'newKeyThatILove');
|
||||
const unEncryptedAdapter = new GridFSBucketAdapter(databaseURI);
|
||||
const fileName1 = 'file1.txt';
|
||||
const data1 = 'hello world';
|
||||
const fileName2 = 'file2.txt';
|
||||
const data2 = 'hello new world';
|
||||
//Store unecrypted files
|
||||
await oldEncryptedAdapter.createFile(fileName1, data1);
|
||||
const oldEncryptedResult1 = await oldEncryptedAdapter.getFileData(fileName1);
|
||||
expect(oldEncryptedResult1.toString('utf8')).toBe(data1);
|
||||
await oldEncryptedAdapter.createFile(fileName2, data2);
|
||||
const oldEncryptedResult2 = await oldEncryptedAdapter.getFileData(fileName2);
|
||||
expect(oldEncryptedResult2.toString('utf8')).toBe(data2);
|
||||
//Inject unecrypted file to see if causes an issue
|
||||
const fileName3 = 'file3.txt';
|
||||
const data3 = 'hello past world';
|
||||
await unEncryptedAdapter.createFile(fileName3, data3, 'text/utf8');
|
||||
//Check if encrypted adapter can read data and make sure it's not the same as unEncrypted adapter
|
||||
const { rotated, notRotated } = await encryptedAdapter.rotateEncryptionKey({
|
||||
oldKey: oldEncryptionKey,
|
||||
fileNames: [fileName1, fileName2],
|
||||
});
|
||||
expect(rotated.length).toEqual(2);
|
||||
expect(
|
||||
rotated.filter(function (value) {
|
||||
return value === fileName1;
|
||||
}).length
|
||||
).toEqual(1);
|
||||
expect(
|
||||
rotated.filter(function (value) {
|
||||
return value === fileName2;
|
||||
}).length
|
||||
).toEqual(1);
|
||||
expect(notRotated.length).toEqual(0);
|
||||
expect(
|
||||
rotated.filter(function (value) {
|
||||
return value === fileName3;
|
||||
}).length
|
||||
).toEqual(0);
|
||||
let result = await encryptedAdapter.getFileData(fileName1);
|
||||
expect(result instanceof Buffer).toBe(true);
|
||||
expect(result.toString('utf-8')).toEqual(data1);
|
||||
let decryptionError1;
|
||||
let encryptedData1;
|
||||
try {
|
||||
encryptedData1 = await oldEncryptedAdapter.getFileData(fileName1);
|
||||
} catch (err) {
|
||||
decryptionError1 = err;
|
||||
}
|
||||
expect(decryptionError1).toMatch('Error');
|
||||
expect(encryptedData1).toBeUndefined();
|
||||
result = await encryptedAdapter.getFileData(fileName2);
|
||||
expect(result instanceof Buffer).toBe(true);
|
||||
expect(result.toString('utf-8')).toEqual(data2);
|
||||
let decryptionError2;
|
||||
let encryptedData2;
|
||||
try {
|
||||
encryptedData2 = await oldEncryptedAdapter.getFileData(fileName2);
|
||||
} catch (err) {
|
||||
decryptionError2 = err;
|
||||
}
|
||||
expect(decryptionError2).toMatch('Error');
|
||||
expect(encryptedData2).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return fileNames of those it can't encrypt with the new key", async () => {
|
||||
const oldEncryptionKey = 'oldKeyThatILoved';
|
||||
const oldEncryptedAdapter = new GridFSBucketAdapter(databaseURI, {}, oldEncryptionKey);
|
||||
const encryptedAdapter = new GridFSBucketAdapter(databaseURI, {}, 'newKeyThatILove');
|
||||
const unEncryptedAdapter = new GridFSBucketAdapter(databaseURI);
|
||||
const fileName1 = 'file1.txt';
|
||||
const data1 = 'hello world';
|
||||
const fileName2 = 'file2.txt';
|
||||
const data2 = 'hello new world';
|
||||
//Store unecrypted files
|
||||
await oldEncryptedAdapter.createFile(fileName1, data1);
|
||||
const oldEncryptedResult1 = await oldEncryptedAdapter.getFileData(fileName1);
|
||||
expect(oldEncryptedResult1.toString('utf8')).toBe(data1);
|
||||
await oldEncryptedAdapter.createFile(fileName2, data2);
|
||||
const oldEncryptedResult2 = await oldEncryptedAdapter.getFileData(fileName2);
|
||||
expect(oldEncryptedResult2.toString('utf8')).toBe(data2);
|
||||
//Inject unecrypted file to see if causes an issue
|
||||
const fileName3 = 'file3.txt';
|
||||
const data3 = 'hello past world';
|
||||
await unEncryptedAdapter.createFile(fileName3, data3, 'text/utf8');
|
||||
//Check if encrypted adapter can read data and make sure it's not the same as unEncrypted adapter
|
||||
const { rotated, notRotated } = await encryptedAdapter.rotateEncryptionKey({
|
||||
oldKey: oldEncryptionKey,
|
||||
});
|
||||
expect(rotated.length).toEqual(2);
|
||||
expect(
|
||||
rotated.filter(function (value) {
|
||||
return value === fileName1;
|
||||
}).length
|
||||
).toEqual(1);
|
||||
expect(
|
||||
rotated.filter(function (value) {
|
||||
return value === fileName2;
|
||||
}).length
|
||||
).toEqual(1);
|
||||
expect(notRotated.length).toEqual(1);
|
||||
expect(
|
||||
notRotated.filter(function (value) {
|
||||
return value === fileName3;
|
||||
}).length
|
||||
).toEqual(1);
|
||||
let result = await encryptedAdapter.getFileData(fileName1);
|
||||
expect(result instanceof Buffer).toBe(true);
|
||||
expect(result.toString('utf-8')).toEqual(data1);
|
||||
let decryptionError1;
|
||||
let encryptedData1;
|
||||
try {
|
||||
encryptedData1 = await oldEncryptedAdapter.getFileData(fileName1);
|
||||
} catch (err) {
|
||||
decryptionError1 = err;
|
||||
}
|
||||
expect(decryptionError1).toMatch('Error');
|
||||
expect(encryptedData1).toBeUndefined();
|
||||
result = await encryptedAdapter.getFileData(fileName2);
|
||||
expect(result instanceof Buffer).toBe(true);
|
||||
expect(result.toString('utf-8')).toEqual(data2);
|
||||
let decryptionError2;
|
||||
let encryptedData2;
|
||||
try {
|
||||
encryptedData2 = await oldEncryptedAdapter.getFileData(fileName2);
|
||||
} catch (err) {
|
||||
decryptionError2 = err;
|
||||
}
|
||||
expect(decryptionError2).toMatch('Error');
|
||||
expect(encryptedData2).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should save metadata', async () => {
|
||||
const gfsAdapter = new GridFSBucketAdapter(databaseURI);
|
||||
const originalString = 'abcdefghi';
|
||||
const metadata = { hello: 'world' };
|
||||
await gfsAdapter.createFile('myFileName', originalString, null, {
|
||||
metadata,
|
||||
});
|
||||
const gfsResult = await gfsAdapter.getFileData('myFileName');
|
||||
expect(gfsResult.toString('utf8')).toBe(originalString);
|
||||
let gfsMetadata = await gfsAdapter.getMetadata('myFileName');
|
||||
expect(gfsMetadata.metadata).toEqual(metadata);
|
||||
|
||||
// Empty json for file not found
|
||||
gfsMetadata = await gfsAdapter.getMetadata('myUnknownFile');
|
||||
expect(gfsMetadata).toEqual({});
|
||||
});
|
||||
|
||||
it('should save metadata with file', async () => {
|
||||
const gfsAdapter = new GridFSBucketAdapter(databaseURI);
|
||||
await reconfigureServer({ filesAdapter: gfsAdapter });
|
||||
const str = 'Hello World!';
|
||||
const data = [];
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
data.push(str.charCodeAt(i));
|
||||
}
|
||||
const metadata = { foo: 'bar' };
|
||||
const file = new Parse.File('hello.txt', data, 'text/plain');
|
||||
file.addMetadata('foo', 'bar');
|
||||
await file.save();
|
||||
let fileData = await gfsAdapter.getMetadata(file.name());
|
||||
expect(fileData.metadata).toEqual(metadata);
|
||||
|
||||
// Can only add metadata on create
|
||||
file.addMetadata('hello', 'world');
|
||||
await file.save();
|
||||
fileData = await gfsAdapter.getMetadata(file.name());
|
||||
expect(fileData.metadata).toEqual(metadata);
|
||||
|
||||
const headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
const response = await request({
|
||||
method: 'GET',
|
||||
headers,
|
||||
url: `http://localhost:8378/1/files/test/metadata/${file.name()}`,
|
||||
});
|
||||
fileData = response.data;
|
||||
expect(fileData.metadata).toEqual(metadata);
|
||||
});
|
||||
|
||||
it('should handle getMetadata error', async () => {
|
||||
const config = Config.get('test');
|
||||
config.filesController.getMetadata = () => Promise.reject();
|
||||
|
||||
const headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
const response = await request({
|
||||
method: 'GET',
|
||||
headers,
|
||||
url: `http://localhost:8378/1/files/test/metadata/filename.txt`,
|
||||
});
|
||||
expect(response.data).toEqual({});
|
||||
});
|
||||
|
||||
it('properly fetches a large file from GridFS', async () => {
|
||||
const gfsAdapter = new GridFSBucketAdapter(databaseURI);
|
||||
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');
|
||||
await gfsAdapter.deleteFile('myFileName');
|
||||
await expectMissingFile(gfsAdapter, 'myFileName');
|
||||
}, 1000000);
|
||||
|
||||
it('properly overrides files', async () => {
|
||||
const gfsAdapter = new GridFSBucketAdapter(databaseURI);
|
||||
await gfsAdapter.createFile('myFileName', 'a simple file');
|
||||
await gfsAdapter.createFile('myFileName', 'an overrided simple file');
|
||||
const data = await gfsAdapter.getFileData('myFileName');
|
||||
expect(data.toString('utf8')).toBe('an overrided simple file');
|
||||
const bucket = await gfsAdapter._getBucket();
|
||||
const documents = await bucket.find({ filename: 'myFileName' }).toArray();
|
||||
expect(documents.length).toBe(2);
|
||||
await gfsAdapter.deleteFile('myFileName');
|
||||
await expectMissingFile(gfsAdapter, 'myFileName');
|
||||
});
|
||||
|
||||
it('handleShutdown, close connection', async () => {
|
||||
const databaseURI = 'mongodb://localhost:27017/parse';
|
||||
const gfsAdapter = new GridFSBucketAdapter(databaseURI);
|
||||
|
||||
const db = await gfsAdapter._connect();
|
||||
const status = await db.admin().serverStatus();
|
||||
expect(status.connections.current > 0).toEqual(true);
|
||||
|
||||
await gfsAdapter.handleShutdown();
|
||||
try {
|
||||
await db.admin().serverStatus();
|
||||
expect(false).toBe(true);
|
||||
} catch (e) {
|
||||
expect(e.message).toEqual('topology was destroyed');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
var MongoClient = require("mongodb").MongoClient;
|
||||
var GridStore = require("mongodb").GridStore;
|
||||
|
||||
var GridStoreAdapter = require("../src/Adapters/Files/GridStoreAdapter").GridStoreAdapter;
|
||||
var Config = require("../src/Config");
|
||||
var FilesController = require('../src/Controllers/FilesController').default;
|
||||
|
||||
|
||||
// Small additional tests to improve overall coverage
|
||||
describe_only_db('mongo')("GridStoreAdapter",() =>{
|
||||
it("should properly instanciate the GridStore when deleting a file", (done) => {
|
||||
|
||||
var databaseURI = 'mongodb://localhost:27017/parse';
|
||||
var config = new Config(Parse.applicationId);
|
||||
var gridStoreAdapter = new GridStoreAdapter(databaseURI);
|
||||
var filesController = new FilesController(gridStoreAdapter);
|
||||
|
||||
// save original unlink before redefinition
|
||||
var originalUnlink = GridStore.prototype.unlink;
|
||||
|
||||
var gridStoreMode;
|
||||
|
||||
// new unlink method that will capture the mode in which GridStore was opened
|
||||
GridStore.prototype.unlink = function() {
|
||||
|
||||
// restore original unlink during first call
|
||||
GridStore.prototype.unlink = originalUnlink;
|
||||
|
||||
gridStoreMode = this.mode;
|
||||
|
||||
return originalUnlink.call(this);
|
||||
};
|
||||
|
||||
|
||||
filesController.createFile(config, 'myFilename.txt', 'my file content', 'text/plain')
|
||||
.then(myFile => {
|
||||
|
||||
return MongoClient.connect(databaseURI)
|
||||
.then(database => {
|
||||
|
||||
// Verify the existance of the fs.files document
|
||||
return database.collection('fs.files').count().then(count => {
|
||||
expect(count).toEqual(1);
|
||||
return database;
|
||||
});
|
||||
})
|
||||
.then(database => {
|
||||
|
||||
// Verify the existance of the fs.files document
|
||||
return database.collection('fs.chunks').count().then(count => {
|
||||
expect(count).toEqual(1);
|
||||
return database.close();
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
return filesController.deleteFile(config, myFile.name);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
return MongoClient.connect(databaseURI)
|
||||
.then(database => {
|
||||
|
||||
// Verify the existance of the fs.files document
|
||||
return database.collection('fs.files').count().then(count => {
|
||||
expect(count).toEqual(0);
|
||||
return database;
|
||||
});
|
||||
})
|
||||
.then(database => {
|
||||
|
||||
// Verify the existance of the fs.files document
|
||||
return database.collection('fs.chunks').count().then(count => {
|
||||
expect(count).toEqual(0);
|
||||
return database.close();
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
// Verify that gridStore was opened in read only mode
|
||||
expect(gridStoreMode).toEqual('r');
|
||||
|
||||
done();
|
||||
})
|
||||
.catch(fail);
|
||||
|
||||
})
|
||||
});
|
||||
@@ -1,111 +0,0 @@
|
||||
const MongoClient = require('mongodb').MongoClient;
|
||||
const GridStore = require('mongodb').GridStore;
|
||||
|
||||
const GridStoreAdapter = require('../lib/Adapters/Files/GridStoreAdapter').GridStoreAdapter;
|
||||
const Config = require('../lib/Config');
|
||||
const FilesController = require('../lib/Controllers/FilesController').default;
|
||||
|
||||
// Small additional tests to improve overall coverage
|
||||
describe_only_db('mongo')('GridStoreAdapter', () => {
|
||||
it('should properly instanciate the GridStore when deleting a file', async done => {
|
||||
const databaseURI = 'mongodb://localhost:27017/parse';
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const gridStoreAdapter = new GridStoreAdapter(databaseURI);
|
||||
const db = await gridStoreAdapter._connect();
|
||||
await db.dropDatabase();
|
||||
const filesController = new FilesController(gridStoreAdapter, Parse.applicationId, {});
|
||||
|
||||
// save original unlink before redefinition
|
||||
const originalUnlink = GridStore.prototype.unlink;
|
||||
|
||||
let gridStoreMode;
|
||||
|
||||
// new unlink method that will capture the mode in which GridStore was opened
|
||||
GridStore.prototype.unlink = function () {
|
||||
// restore original unlink during first call
|
||||
GridStore.prototype.unlink = originalUnlink;
|
||||
|
||||
gridStoreMode = this.mode;
|
||||
|
||||
return originalUnlink.call(this);
|
||||
};
|
||||
|
||||
filesController
|
||||
.createFile(config, 'myFilename.txt', 'my file content', 'text/plain')
|
||||
.then(myFile => {
|
||||
return MongoClient.connect(databaseURI)
|
||||
.then(client => {
|
||||
const database = client.db(client.s.options.dbName);
|
||||
// Verify the existance of the fs.files document
|
||||
return database
|
||||
.collection('fs.files')
|
||||
.count()
|
||||
.then(count => {
|
||||
expect(count).toEqual(1);
|
||||
return { database, client };
|
||||
});
|
||||
})
|
||||
.then(({ database, client }) => {
|
||||
// Verify the existance of the fs.files document
|
||||
return database
|
||||
.collection('fs.chunks')
|
||||
.count()
|
||||
.then(count => {
|
||||
expect(count).toEqual(1);
|
||||
return client.close();
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
return filesController.deleteFile(config, myFile.name);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
return MongoClient.connect(databaseURI)
|
||||
.then(client => {
|
||||
const database = client.db(client.s.options.dbName);
|
||||
// Verify the existance of the fs.files document
|
||||
return database
|
||||
.collection('fs.files')
|
||||
.count()
|
||||
.then(count => {
|
||||
expect(count).toEqual(0);
|
||||
return { database, client };
|
||||
});
|
||||
})
|
||||
.then(({ database, client }) => {
|
||||
// Verify the existance of the fs.files document
|
||||
return database
|
||||
.collection('fs.chunks')
|
||||
.count()
|
||||
.then(count => {
|
||||
expect(count).toEqual(0);
|
||||
return client.close();
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
// Verify that gridStore was opened in read only mode
|
||||
expect(gridStoreMode).toEqual('r');
|
||||
|
||||
done();
|
||||
})
|
||||
.catch(fail);
|
||||
});
|
||||
|
||||
it('handleShutdown, close connection', async () => {
|
||||
const databaseURI = 'mongodb://localhost:27017/parse';
|
||||
const gridStoreAdapter = new GridStoreAdapter(databaseURI);
|
||||
|
||||
const db = await gridStoreAdapter._connect();
|
||||
const status = await db.admin().serverStatus();
|
||||
expect(status.connections.current > 0).toEqual(true);
|
||||
|
||||
await gridStoreAdapter.handleShutdown();
|
||||
try {
|
||||
await db.admin().serverStatus();
|
||||
expect(false).toBe(true);
|
||||
} catch (e) {
|
||||
expect(e.message).toEqual('topology was destroyed');
|
||||
}
|
||||
});
|
||||
});
|
||||
+174
-139
@@ -1,97 +1,116 @@
|
||||
'use strict';
|
||||
|
||||
const httpRequest = require('../lib/cloud-code/httpRequest'),
|
||||
HTTPResponse = require('../lib/cloud-code/HTTPResponse').default,
|
||||
var httpRequest = require("../src/cloud-code/httpRequest"),
|
||||
HTTPResponse = require('../src/cloud-code/HTTPResponse').default,
|
||||
bodyParser = require('body-parser'),
|
||||
express = require('express');
|
||||
express = require("express");
|
||||
|
||||
const port = 13371;
|
||||
const httpRequestServer = 'http://localhost:' + port;
|
||||
var port = 13371;
|
||||
var httpRequestServer = "http://localhost:"+port;
|
||||
|
||||
function startServer(done) {
|
||||
const app = express();
|
||||
app.use(bodyParser.json({ type: '*/*' }));
|
||||
app.get('/hello', function (req, res) {
|
||||
res.json({ response: 'OK' });
|
||||
});
|
||||
var app = express();
|
||||
app.use(bodyParser.json({ 'type': '*/*' }));
|
||||
app.get("/hello", function(req, res){
|
||||
res.json({response: "OK"});
|
||||
});
|
||||
|
||||
app.get('/404', function (req, res) {
|
||||
res.status(404);
|
||||
res.send('NO');
|
||||
});
|
||||
app.get("/404", function(req, res){
|
||||
res.status(404);
|
||||
res.send("NO");
|
||||
});
|
||||
|
||||
app.get('/301', function (req, res) {
|
||||
res.status(301);
|
||||
res.location('/hello');
|
||||
res.send();
|
||||
});
|
||||
app.get("/301", function(req, res){
|
||||
res.status(301);
|
||||
res.location("/hello");
|
||||
res.send();
|
||||
});
|
||||
|
||||
app.post('/echo', function (req, res) {
|
||||
res.json(req.body);
|
||||
});
|
||||
app.post('/echo', function(req, res){
|
||||
res.json(req.body);
|
||||
});
|
||||
|
||||
app.get('/qs', function (req, res) {
|
||||
res.json(req.query);
|
||||
});
|
||||
app.get('/qs', function(req, res){
|
||||
res.json(req.query);
|
||||
});
|
||||
|
||||
return app.listen(13371, undefined, done);
|
||||
}
|
||||
app.listen(13371);
|
||||
|
||||
describe('httpRequest', () => {
|
||||
let server;
|
||||
beforeAll(done => {
|
||||
server = startServer(done);
|
||||
});
|
||||
|
||||
afterAll(done => {
|
||||
server.close(done);
|
||||
});
|
||||
|
||||
it('should do /hello', done => {
|
||||
describe("httpRequest", () => {
|
||||
it("should do /hello", (done) => {
|
||||
httpRequest({
|
||||
url: httpRequestServer + '/hello',
|
||||
}).then(function (httpResponse) {
|
||||
url: httpRequestServer+"/hello"
|
||||
}).then(function(httpResponse){
|
||||
expect(httpResponse.status).toBe(200);
|
||||
expect(httpResponse.buffer).toEqual(new Buffer('{"response":"OK"}'));
|
||||
expect(httpResponse.text).toEqual('{"response":"OK"}');
|
||||
expect(httpResponse.data.response).toEqual('OK');
|
||||
expect(httpResponse.data.response).toEqual("OK");
|
||||
done();
|
||||
}, done.fail);
|
||||
}, function(){
|
||||
fail("should not fail");
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('should do not follow redirects by default', done => {
|
||||
it("should do /hello with callback and promises", (done) => {
|
||||
var calls = 0;
|
||||
httpRequest({
|
||||
url: httpRequestServer + '/301',
|
||||
}).then(function (httpResponse) {
|
||||
url: httpRequestServer+"/hello",
|
||||
success: function() { calls++; },
|
||||
error: function() { calls++; }
|
||||
}).then(function(httpResponse){
|
||||
expect(calls).toBe(1);
|
||||
expect(httpResponse.status).toBe(200);
|
||||
expect(httpResponse.buffer).toEqual(new Buffer('{"response":"OK"}'));
|
||||
expect(httpResponse.text).toEqual('{"response":"OK"}');
|
||||
expect(httpResponse.data.response).toEqual("OK");
|
||||
done();
|
||||
}, function(){
|
||||
fail("should not fail");
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it("should do not follow redirects by default", (done) => {
|
||||
|
||||
httpRequest({
|
||||
url: httpRequestServer+"/301"
|
||||
}).then(function(httpResponse){
|
||||
expect(httpResponse.status).toBe(301);
|
||||
done();
|
||||
}, done.fail);
|
||||
}, function(){
|
||||
fail("should not fail");
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('should follow redirects when set', done => {
|
||||
it("should follow redirects when set", (done) => {
|
||||
|
||||
httpRequest({
|
||||
url: httpRequestServer + '/301',
|
||||
followRedirects: true,
|
||||
}).then(function (httpResponse) {
|
||||
url: httpRequestServer+"/301",
|
||||
followRedirects: true
|
||||
}).then(function(httpResponse){
|
||||
expect(httpResponse.status).toBe(200);
|
||||
expect(httpResponse.buffer).toEqual(new Buffer('{"response":"OK"}'));
|
||||
expect(httpResponse.text).toEqual('{"response":"OK"}');
|
||||
expect(httpResponse.data.response).toEqual('OK');
|
||||
expect(httpResponse.data.response).toEqual("OK");
|
||||
done();
|
||||
}, done.fail);
|
||||
}, function(){
|
||||
fail("should not fail");
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('should fail on 404', done => {
|
||||
let calls = 0;
|
||||
it("should fail on 404", (done) => {
|
||||
var calls = 0;
|
||||
httpRequest({
|
||||
url: httpRequestServer + '/404',
|
||||
}).then(
|
||||
function () {
|
||||
url: httpRequestServer+"/404",
|
||||
success: function() {
|
||||
calls++;
|
||||
fail('should not succeed');
|
||||
fail("should not succeed");
|
||||
done();
|
||||
},
|
||||
function (httpResponse) {
|
||||
error: function(httpResponse) {
|
||||
calls++;
|
||||
expect(calls).toBe(1);
|
||||
expect(httpResponse.status).toBe(404);
|
||||
@@ -100,114 +119,131 @@ describe('httpRequest', () => {
|
||||
expect(httpResponse.data).toBe(undefined);
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
it('should post on echo', done => {
|
||||
it("should fail on 404", (done) => {
|
||||
httpRequest({
|
||||
method: 'POST',
|
||||
url: httpRequestServer + '/echo',
|
||||
url: httpRequestServer+"/404",
|
||||
}).then(function(){
|
||||
fail("should not succeed");
|
||||
done();
|
||||
}, function(httpResponse){
|
||||
expect(httpResponse.status).toBe(404);
|
||||
expect(httpResponse.buffer).toEqual(new Buffer('NO'));
|
||||
expect(httpResponse.text).toEqual('NO');
|
||||
expect(httpResponse.data).toBe(undefined);
|
||||
done();
|
||||
})
|
||||
})
|
||||
|
||||
it("should post on echo", (done) => {
|
||||
var calls = 0;
|
||||
httpRequest({
|
||||
method: "POST",
|
||||
url: httpRequestServer+"/echo",
|
||||
body: {
|
||||
foo: 'bar',
|
||||
foo: "bar"
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}).then(
|
||||
function (httpResponse) {
|
||||
expect(httpResponse.status).toBe(200);
|
||||
expect(httpResponse.data).toEqual({ foo: 'bar' });
|
||||
done();
|
||||
},
|
||||
function () {
|
||||
fail('should not fail');
|
||||
done();
|
||||
}
|
||||
);
|
||||
success: function() { calls++; },
|
||||
error: function() { calls++; }
|
||||
}).then(function(httpResponse){
|
||||
expect(calls).toBe(1);
|
||||
expect(httpResponse.status).toBe(200);
|
||||
expect(httpResponse.data).toEqual({foo: "bar"});
|
||||
done();
|
||||
}, function(){
|
||||
fail("should not fail");
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('should encode a query string body by default', done => {
|
||||
it("should encode a query string body by default", (done) => {
|
||||
const options = {
|
||||
body: { foo: 'bar' },
|
||||
};
|
||||
body: {"foo": "bar"},
|
||||
}
|
||||
const result = httpRequest.encodeBody(options);
|
||||
expect(result.body).toEqual('foo=bar');
|
||||
expect(result.headers['Content-Type']).toEqual('application/x-www-form-urlencoded');
|
||||
done();
|
||||
});
|
||||
|
||||
it('should encode a JSON body', done => {
|
||||
})
|
||||
|
||||
it("should encode a JSON body", (done) => {
|
||||
const options = {
|
||||
body: { foo: 'bar' },
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
};
|
||||
body: {"foo": "bar"},
|
||||
headers: {'Content-Type': 'application/json'}
|
||||
}
|
||||
const result = httpRequest.encodeBody(options);
|
||||
expect(result.body).toEqual('{"foo":"bar"}');
|
||||
done();
|
||||
});
|
||||
it('should encode a www-form body', done => {
|
||||
|
||||
})
|
||||
it("should encode a www-form body", (done) => {
|
||||
const options = {
|
||||
body: { foo: 'bar', bar: 'baz' },
|
||||
headers: { 'cOntent-tYpe': 'application/x-www-form-urlencoded' },
|
||||
};
|
||||
body: {"foo": "bar", "bar": "baz"},
|
||||
headers: {'cOntent-tYpe': 'application/x-www-form-urlencoded'}
|
||||
}
|
||||
const result = httpRequest.encodeBody(options);
|
||||
expect(result.body).toEqual('foo=bar&bar=baz');
|
||||
expect(result.body).toEqual("foo=bar&bar=baz");
|
||||
done();
|
||||
});
|
||||
it('should not encode a wrong content type', done => {
|
||||
it("should not encode a wrong content type", (done) => {
|
||||
const options = {
|
||||
body: { foo: 'bar', bar: 'baz' },
|
||||
headers: { 'cOntent-tYpe': 'mime/jpeg' },
|
||||
};
|
||||
body:{"foo": "bar", "bar": "baz"},
|
||||
headers: {'cOntent-tYpe': 'mime/jpeg'}
|
||||
}
|
||||
const result = httpRequest.encodeBody(options);
|
||||
expect(result.body).toEqual({ foo: 'bar', bar: 'baz' });
|
||||
expect(result.body).toEqual({"foo": "bar", "bar": "baz"});
|
||||
done();
|
||||
});
|
||||
|
||||
it('should fail gracefully', done => {
|
||||
it("should fail gracefully", (done) => {
|
||||
httpRequest({
|
||||
url: 'http://not a good url',
|
||||
}).then(done.fail, function (error) {
|
||||
expect(error).not.toBeUndefined();
|
||||
expect(error).not.toBeNull();
|
||||
done();
|
||||
url: "http://not a good url",
|
||||
success: function() {
|
||||
fail("should not succeed");
|
||||
done();
|
||||
},
|
||||
error: function(error) {
|
||||
expect(error).not.toBeUndefined();
|
||||
expect(error).not.toBeNull();
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should params object to query string', done => {
|
||||
it("should params object to query string", (done) => {
|
||||
httpRequest({
|
||||
url: httpRequestServer + '/qs',
|
||||
url: httpRequestServer+"/qs",
|
||||
params: {
|
||||
foo: 'bar',
|
||||
},
|
||||
}).then(
|
||||
function (httpResponse) {
|
||||
expect(httpResponse.status).toBe(200);
|
||||
expect(httpResponse.data).toEqual({ foo: 'bar' });
|
||||
done();
|
||||
},
|
||||
function () {
|
||||
fail('should not fail');
|
||||
done();
|
||||
foo: "bar"
|
||||
}
|
||||
);
|
||||
}).then(function(httpResponse){
|
||||
expect(httpResponse.status).toBe(200);
|
||||
expect(httpResponse.data).toEqual({foo: "bar"});
|
||||
done();
|
||||
}, function(){
|
||||
fail("should not fail");
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('should params string to query string', done => {
|
||||
it("should params string to query string", (done) => {
|
||||
httpRequest({
|
||||
url: httpRequestServer + '/qs',
|
||||
params: 'foo=bar&foo2=bar2',
|
||||
}).then(
|
||||
function (httpResponse) {
|
||||
expect(httpResponse.status).toBe(200);
|
||||
expect(httpResponse.data).toEqual({ foo: 'bar', foo2: 'bar2' });
|
||||
done();
|
||||
},
|
||||
function () {
|
||||
fail('should not fail');
|
||||
done();
|
||||
}
|
||||
);
|
||||
url: httpRequestServer+"/qs",
|
||||
params: "foo=bar&foo2=bar2"
|
||||
}).then(function(httpResponse){
|
||||
expect(httpResponse.status).toBe(200);
|
||||
expect(httpResponse.data).toEqual({foo: "bar", foo2: 'bar2'});
|
||||
done();
|
||||
}, function(){
|
||||
fail("should not fail");
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('should not crash with undefined body', () => {
|
||||
@@ -232,17 +268,17 @@ describe('httpRequest', () => {
|
||||
});
|
||||
|
||||
it('serialized httpResponse correctly with body object', () => {
|
||||
const httpResponse = new HTTPResponse({}, { foo: 'bar' });
|
||||
const httpResponse = new HTTPResponse({}, {foo: "bar"});
|
||||
Parse._encode(httpResponse);
|
||||
const serialized = JSON.stringify(httpResponse);
|
||||
const result = JSON.parse(serialized);
|
||||
|
||||
expect(httpResponse.text).toEqual('{"foo":"bar"}');
|
||||
expect(httpResponse.data).toEqual({ foo: 'bar' });
|
||||
expect(httpResponse.body).toEqual({ foo: 'bar' });
|
||||
expect(httpResponse.data).toEqual({foo: 'bar'});
|
||||
expect(httpResponse.body).toEqual({foo: 'bar'});
|
||||
|
||||
expect(result.text).toEqual('{"foo":"bar"}');
|
||||
expect(result.data).toEqual({ foo: 'bar' });
|
||||
expect(result.data).toEqual({foo: 'bar'});
|
||||
expect(result.body).toEqual(undefined);
|
||||
});
|
||||
|
||||
@@ -263,17 +299,15 @@ describe('httpRequest', () => {
|
||||
const serialized = JSON.stringify(httpResponse);
|
||||
const result = JSON.parse(serialized);
|
||||
expect(result.text).toEqual('{"foo":"bar"}');
|
||||
expect(result.data).toEqual({ foo: 'bar' });
|
||||
expect(result.data).toEqual({foo: 'bar'});
|
||||
});
|
||||
|
||||
it('serialized httpResponse with Parse._encode should be allright', () => {
|
||||
const json = '{"foo":"bar"}';
|
||||
const httpResponse = new HTTPResponse({}, new Buffer(json));
|
||||
const encoded = Parse._encode(httpResponse);
|
||||
let foundData,
|
||||
foundText,
|
||||
foundBody = false;
|
||||
for (const key in encoded) {
|
||||
let foundData, foundText, foundBody = false;
|
||||
for(var key in encoded) {
|
||||
if (key == 'data') {
|
||||
foundData = true;
|
||||
}
|
||||
@@ -288,4 +322,5 @@ describe('httpRequest', () => {
|
||||
expect(foundText).toBe(true);
|
||||
expect(foundBody).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -1,243 +0,0 @@
|
||||
'use strict';
|
||||
const Config = require('../lib/Config');
|
||||
const Definitions = require('../lib/Options/Definitions');
|
||||
const request = require('../lib/request');
|
||||
const rest = require('../lib/rest');
|
||||
const auth = require('../lib/Auth');
|
||||
const uuid = require('uuid');
|
||||
|
||||
describe_only_db('mongo')('Idempotency', () => {
|
||||
// Parameters
|
||||
/** Enable TTL expiration simulated by removing entry instead of waiting for MongoDB TTL monitor which
|
||||
runs only every 60s, so it can take up to 119s until entry removal - ain't nobody got time for that */
|
||||
const SIMULATE_TTL = true;
|
||||
// Helpers
|
||||
async function deleteRequestEntry(reqId) {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const res = await rest.find(
|
||||
config,
|
||||
auth.master(config),
|
||||
'_Idempotency',
|
||||
{ reqId: reqId },
|
||||
{ limit: 1 }
|
||||
);
|
||||
await rest.del(config, auth.master(config), '_Idempotency', res.results[0].objectId);
|
||||
}
|
||||
async function setup(options) {
|
||||
await reconfigureServer({
|
||||
appId: Parse.applicationId,
|
||||
masterKey: Parse.masterKey,
|
||||
serverURL: Parse.serverURL,
|
||||
idempotencyOptions: options,
|
||||
});
|
||||
}
|
||||
// Setups
|
||||
beforeEach(async () => {
|
||||
if (SIMULATE_TTL) {
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 200000;
|
||||
}
|
||||
await setup({
|
||||
paths: ['functions/.*', 'jobs/.*', 'classes/.*', 'users', 'installations'],
|
||||
ttl: 30,
|
||||
});
|
||||
});
|
||||
// Tests
|
||||
it('should enforce idempotency for cloud code function', async () => {
|
||||
let counter = 0;
|
||||
Parse.Cloud.define('myFunction', () => {
|
||||
counter++;
|
||||
});
|
||||
const params = {
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/functions/myFunction',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-Master-Key': Parse.masterKey,
|
||||
'X-Parse-Request-Id': 'abc-123',
|
||||
},
|
||||
};
|
||||
expect(Config.get(Parse.applicationId).idempotencyOptions.ttl).toBe(30);
|
||||
await request(params);
|
||||
await request(params).then(fail, e => {
|
||||
expect(e.status).toEqual(400);
|
||||
expect(e.data.error).toEqual('Duplicate request');
|
||||
});
|
||||
expect(counter).toBe(1);
|
||||
});
|
||||
|
||||
it('should delete request entry after TTL', async () => {
|
||||
let counter = 0;
|
||||
Parse.Cloud.define('myFunction', () => {
|
||||
counter++;
|
||||
});
|
||||
const params = {
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/functions/myFunction',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-Master-Key': Parse.masterKey,
|
||||
'X-Parse-Request-Id': 'abc-123',
|
||||
},
|
||||
};
|
||||
await expectAsync(request(params)).toBeResolved();
|
||||
if (SIMULATE_TTL) {
|
||||
await deleteRequestEntry('abc-123');
|
||||
} else {
|
||||
await new Promise(resolve => setTimeout(resolve, 130000));
|
||||
}
|
||||
await expectAsync(request(params)).toBeResolved();
|
||||
expect(counter).toBe(2);
|
||||
});
|
||||
|
||||
it('should enforce idempotency for cloud code jobs', async () => {
|
||||
let counter = 0;
|
||||
Parse.Cloud.job('myJob', () => {
|
||||
counter++;
|
||||
});
|
||||
const params = {
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/jobs/myJob',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-Master-Key': Parse.masterKey,
|
||||
'X-Parse-Request-Id': 'abc-123',
|
||||
},
|
||||
};
|
||||
await expectAsync(request(params)).toBeResolved();
|
||||
await request(params).then(fail, e => {
|
||||
expect(e.status).toEqual(400);
|
||||
expect(e.data.error).toEqual('Duplicate request');
|
||||
});
|
||||
expect(counter).toBe(1);
|
||||
});
|
||||
|
||||
it('should enforce idempotency for class object creation', async () => {
|
||||
let counter = 0;
|
||||
Parse.Cloud.afterSave('MyClass', () => {
|
||||
counter++;
|
||||
});
|
||||
const params = {
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/classes/MyClass',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-Master-Key': Parse.masterKey,
|
||||
'X-Parse-Request-Id': 'abc-123',
|
||||
},
|
||||
};
|
||||
await expectAsync(request(params)).toBeResolved();
|
||||
await request(params).then(fail, e => {
|
||||
expect(e.status).toEqual(400);
|
||||
expect(e.data.error).toEqual('Duplicate request');
|
||||
});
|
||||
expect(counter).toBe(1);
|
||||
});
|
||||
|
||||
it('should enforce idempotency for user object creation', async () => {
|
||||
let counter = 0;
|
||||
Parse.Cloud.afterSave('_User', () => {
|
||||
counter++;
|
||||
});
|
||||
const params = {
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/users',
|
||||
body: {
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
},
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-Master-Key': Parse.masterKey,
|
||||
'X-Parse-Request-Id': 'abc-123',
|
||||
},
|
||||
};
|
||||
await expectAsync(request(params)).toBeResolved();
|
||||
await request(params).then(fail, e => {
|
||||
expect(e.status).toEqual(400);
|
||||
expect(e.data.error).toEqual('Duplicate request');
|
||||
});
|
||||
expect(counter).toBe(1);
|
||||
});
|
||||
|
||||
it('should enforce idempotency for installation object creation', async () => {
|
||||
let counter = 0;
|
||||
Parse.Cloud.afterSave('_Installation', () => {
|
||||
counter++;
|
||||
});
|
||||
const params = {
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/installations',
|
||||
body: {
|
||||
installationId: '1',
|
||||
deviceType: 'ios',
|
||||
},
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-Master-Key': Parse.masterKey,
|
||||
'X-Parse-Request-Id': 'abc-123',
|
||||
},
|
||||
};
|
||||
await expectAsync(request(params)).toBeResolved();
|
||||
await request(params).then(fail, e => {
|
||||
expect(e.status).toEqual(400);
|
||||
expect(e.data.error).toEqual('Duplicate request');
|
||||
});
|
||||
expect(counter).toBe(1);
|
||||
});
|
||||
|
||||
it('should not interfere with calls of different request ID', async () => {
|
||||
let counter = 0;
|
||||
Parse.Cloud.afterSave('MyClass', () => {
|
||||
counter++;
|
||||
});
|
||||
const promises = [...Array(100).keys()].map(() => {
|
||||
const params = {
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/classes/MyClass',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-Master-Key': Parse.masterKey,
|
||||
'X-Parse-Request-Id': uuid.v4(),
|
||||
},
|
||||
};
|
||||
return request(params);
|
||||
});
|
||||
await expectAsync(Promise.all(promises)).toBeResolved();
|
||||
expect(counter).toBe(100);
|
||||
});
|
||||
|
||||
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 = {
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/functions/myFunction',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-Master-Key': Parse.masterKey,
|
||||
'X-Parse-Request-Id': 'abc-123',
|
||||
},
|
||||
};
|
||||
await request(params).then(fail, e => {
|
||||
expect(e.status).toEqual(400);
|
||||
expect(e.data.error).toEqual('some other error');
|
||||
});
|
||||
});
|
||||
|
||||
it('should use default configuration when none is set', async () => {
|
||||
await setup({});
|
||||
expect(Config.get(Parse.applicationId).idempotencyOptions.ttl).toBe(
|
||||
Definitions.IdempotencyOptions.ttl.default
|
||||
);
|
||||
expect(Config.get(Parse.applicationId).idempotencyOptions.paths).toBe(
|
||||
Definitions.IdempotencyOptions.paths.default
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw on invalid configuration', async () => {
|
||||
await expectAsync(setup({ paths: 1 })).toBeRejected();
|
||||
await expectAsync(setup({ ttl: 'a' })).toBeRejected();
|
||||
await expectAsync(setup({ ttl: 0 })).toBeRejected();
|
||||
await expectAsync(setup({ ttl: -1 })).toBeRejected();
|
||||
});
|
||||
});
|
||||
+26
-23
@@ -1,40 +1,42 @@
|
||||
const InMemoryCache = require('../lib/Adapters/Cache/InMemoryCache').default;
|
||||
const InMemoryCache = require('../src/Adapters/Cache/InMemoryCache').default;
|
||||
|
||||
describe('InMemoryCache', function () {
|
||||
const BASE_TTL = {
|
||||
ttl: 100,
|
||||
};
|
||||
const NO_EXPIRE_TTL = {
|
||||
ttl: NaN,
|
||||
};
|
||||
const KEY = 'hello';
|
||||
const KEY_2 = KEY + '_2';
|
||||
|
||||
const VALUE = 'world';
|
||||
describe('InMemoryCache', function() {
|
||||
var BASE_TTL = {
|
||||
ttl: 10
|
||||
};
|
||||
var NO_EXPIRE_TTL = {
|
||||
ttl: NaN
|
||||
};
|
||||
var KEY = 'hello';
|
||||
var KEY_2 = KEY + '_2';
|
||||
|
||||
var VALUE = 'world';
|
||||
|
||||
|
||||
function wait(sleep) {
|
||||
return new Promise(function (resolve) {
|
||||
return new Promise(function(resolve) {
|
||||
setTimeout(resolve, sleep);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
it('should destroy a expire items in the cache', done => {
|
||||
const cache = new InMemoryCache(BASE_TTL);
|
||||
it('should destroy a expire items in the cache', (done) => {
|
||||
var cache = new InMemoryCache(BASE_TTL);
|
||||
|
||||
cache.put(KEY, VALUE);
|
||||
|
||||
let value = cache.get(KEY);
|
||||
var value = cache.get(KEY);
|
||||
expect(value).toEqual(VALUE);
|
||||
|
||||
wait(BASE_TTL.ttl * 10).then(() => {
|
||||
value = cache.get(KEY);
|
||||
wait(BASE_TTL.ttl * 5).then(() => {
|
||||
value = cache.get(KEY)
|
||||
expect(value).toEqual(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete items', done => {
|
||||
const cache = new InMemoryCache(NO_EXPIRE_TTL);
|
||||
it('should delete items', (done) => {
|
||||
var cache = new InMemoryCache(NO_EXPIRE_TTL);
|
||||
cache.put(KEY, VALUE);
|
||||
cache.put(KEY_2, VALUE);
|
||||
expect(cache.get(KEY)).toEqual(VALUE);
|
||||
@@ -50,8 +52,8 @@ describe('InMemoryCache', function () {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should clear all items', done => {
|
||||
const cache = new InMemoryCache(NO_EXPIRE_TTL);
|
||||
it('should clear all items', (done) => {
|
||||
var cache = new InMemoryCache(NO_EXPIRE_TTL);
|
||||
cache.put(KEY, VALUE);
|
||||
cache.put(KEY_2, VALUE);
|
||||
|
||||
@@ -65,7 +67,8 @@ describe('InMemoryCache', function () {
|
||||
});
|
||||
|
||||
it('should deafult TTL to 5 seconds', () => {
|
||||
const cache = new InMemoryCache({});
|
||||
var cache = new InMemoryCache({});
|
||||
expect(cache.ttl).toEqual(5 * 1000);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -1,53 +1,57 @@
|
||||
const InMemoryCacheAdapter = require('../lib/Adapters/Cache/InMemoryCacheAdapter').default;
|
||||
var InMemoryCacheAdapter = require('../src/Adapters/Cache/InMemoryCacheAdapter').default;
|
||||
|
||||
describe('InMemoryCacheAdapter', function () {
|
||||
const KEY = 'hello';
|
||||
const VALUE = 'world';
|
||||
describe('InMemoryCacheAdapter', function() {
|
||||
var KEY = 'hello';
|
||||
var VALUE = 'world';
|
||||
|
||||
function wait(sleep) {
|
||||
return new Promise(function (resolve) {
|
||||
return new Promise(function(resolve) {
|
||||
setTimeout(resolve, sleep);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
it('should expose promisifyed methods', done => {
|
||||
const cache = new InMemoryCacheAdapter({
|
||||
ttl: NaN,
|
||||
it('should expose promisifyed methods', (done) => {
|
||||
var cache = new InMemoryCacheAdapter({
|
||||
ttl: NaN
|
||||
});
|
||||
|
||||
// Verify all methods return promises.
|
||||
Promise.all([cache.put(KEY, VALUE), cache.del(KEY), cache.get(KEY), cache.clear()]).then(() => {
|
||||
Promise.all([
|
||||
cache.put(KEY, VALUE),
|
||||
cache.del(KEY),
|
||||
cache.get(KEY),
|
||||
cache.clear()
|
||||
]).then(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should get/set/clear', done => {
|
||||
const cache = new InMemoryCacheAdapter({
|
||||
ttl: NaN,
|
||||
it('should get/set/clear', (done) => {
|
||||
var cache = new InMemoryCacheAdapter({
|
||||
ttl: NaN
|
||||
});
|
||||
|
||||
cache
|
||||
.put(KEY, VALUE)
|
||||
cache.put(KEY, VALUE)
|
||||
.then(() => cache.get(KEY))
|
||||
.then(value => expect(value).toEqual(VALUE))
|
||||
.then((value) => expect(value).toEqual(VALUE))
|
||||
.then(() => cache.clear())
|
||||
.then(() => cache.get(KEY))
|
||||
.then(value => expect(value).toEqual(null))
|
||||
.then((value) => expect(value).toEqual(null))
|
||||
.then(done);
|
||||
});
|
||||
|
||||
it('should expire after ttl', done => {
|
||||
const cache = new InMemoryCacheAdapter({
|
||||
ttl: 10,
|
||||
it('should expire after ttl', (done) => {
|
||||
var cache = new InMemoryCacheAdapter({
|
||||
ttl: 10
|
||||
});
|
||||
|
||||
cache
|
||||
.put(KEY, VALUE)
|
||||
cache.put(KEY, VALUE)
|
||||
.then(() => cache.get(KEY))
|
||||
.then(value => expect(value).toEqual(VALUE))
|
||||
.then((value) => expect(value).toEqual(VALUE))
|
||||
.then(wait.bind(null, 50))
|
||||
.then(() => cache.get(KEY))
|
||||
.then(value => expect(value).toEqual(null))
|
||||
.then((value) => expect(value).toEqual(null))
|
||||
.then(done);
|
||||
});
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
+155
-209
@@ -1,247 +1,193 @@
|
||||
const auth = require('../lib/Auth');
|
||||
const Config = require('../lib/Config');
|
||||
const rest = require('../lib/rest');
|
||||
const InstallationsRouter = require('../lib/Routers/InstallationsRouter').InstallationsRouter;
|
||||
var auth = require('../src/Auth');
|
||||
var Config = require('../src/Config');
|
||||
var rest = require('../src/rest');
|
||||
var InstallationsRouter = require('../src/Routers/InstallationsRouter').InstallationsRouter;
|
||||
|
||||
describe('InstallationsRouter', () => {
|
||||
it('uses find condition from request.body', done => {
|
||||
const config = Config.get('test');
|
||||
const androidDeviceRequest = {
|
||||
installationId: '12345678-abcd-abcd-abcd-123456789abc',
|
||||
deviceType: 'android',
|
||||
it('uses find condition from request.body', (done) => {
|
||||
var config = new Config('test');
|
||||
var androidDeviceRequest = {
|
||||
'installationId': '12345678-abcd-abcd-abcd-123456789abc',
|
||||
'deviceType': 'android'
|
||||
};
|
||||
const iosDeviceRequest = {
|
||||
installationId: '12345678-abcd-abcd-abcd-123456789abd',
|
||||
deviceType: 'ios',
|
||||
var iosDeviceRequest = {
|
||||
'installationId': '12345678-abcd-abcd-abcd-123456789abd',
|
||||
'deviceType': 'ios'
|
||||
};
|
||||
const request = {
|
||||
var request = {
|
||||
config: config,
|
||||
auth: auth.master(config),
|
||||
body: {
|
||||
where: {
|
||||
deviceType: 'android',
|
||||
},
|
||||
deviceType: 'android'
|
||||
}
|
||||
},
|
||||
query: {},
|
||||
info: {},
|
||||
info: {}
|
||||
};
|
||||
|
||||
const router = new InstallationsRouter();
|
||||
rest
|
||||
.create(config, auth.nobody(config), '_Installation', androidDeviceRequest)
|
||||
.then(() => {
|
||||
return rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest);
|
||||
})
|
||||
.then(() => {
|
||||
return router.handleFind(request);
|
||||
})
|
||||
.then(res => {
|
||||
const results = res.response.results;
|
||||
expect(results.length).toEqual(1);
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
fail(JSON.stringify(err));
|
||||
done();
|
||||
});
|
||||
var router = new InstallationsRouter();
|
||||
rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest)
|
||||
.then(() => {
|
||||
return rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest);
|
||||
}).then(() => {
|
||||
return router.handleFind(request);
|
||||
}).then((res) => {
|
||||
var results = res.response.results;
|
||||
expect(results.length).toEqual(1);
|
||||
done();
|
||||
}).catch((err) => {
|
||||
fail(JSON.stringify(err));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('uses find condition from request.query', done => {
|
||||
const config = Config.get('test');
|
||||
const androidDeviceRequest = {
|
||||
installationId: '12345678-abcd-abcd-abcd-123456789abc',
|
||||
deviceType: 'android',
|
||||
it('uses find condition from request.query', (done) => {
|
||||
var config = new Config('test');
|
||||
var androidDeviceRequest = {
|
||||
'installationId': '12345678-abcd-abcd-abcd-123456789abc',
|
||||
'deviceType': 'android'
|
||||
};
|
||||
const iosDeviceRequest = {
|
||||
installationId: '12345678-abcd-abcd-abcd-123456789abd',
|
||||
deviceType: 'ios',
|
||||
var iosDeviceRequest = {
|
||||
'installationId': '12345678-abcd-abcd-abcd-123456789abd',
|
||||
'deviceType': 'ios'
|
||||
};
|
||||
const request = {
|
||||
var request = {
|
||||
config: config,
|
||||
auth: auth.master(config),
|
||||
body: {},
|
||||
query: {
|
||||
where: {
|
||||
deviceType: 'android',
|
||||
},
|
||||
deviceType: 'android'
|
||||
}
|
||||
},
|
||||
info: {},
|
||||
info: {}
|
||||
};
|
||||
|
||||
const router = new InstallationsRouter();
|
||||
rest
|
||||
.create(config, auth.nobody(config), '_Installation', androidDeviceRequest)
|
||||
.then(() => {
|
||||
return rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest);
|
||||
})
|
||||
.then(() => {
|
||||
return router.handleFind(request);
|
||||
})
|
||||
.then(res => {
|
||||
const results = res.response.results;
|
||||
expect(results.length).toEqual(1);
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
jfail(err);
|
||||
done();
|
||||
});
|
||||
var router = new InstallationsRouter();
|
||||
rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest)
|
||||
.then(() => {
|
||||
return rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest);
|
||||
}).then(() => {
|
||||
return router.handleFind(request);
|
||||
}).then((res) => {
|
||||
var results = res.response.results;
|
||||
expect(results.length).toEqual(1);
|
||||
done();
|
||||
}).catch((err) => {
|
||||
jfail(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('query installations with limit = 0', done => {
|
||||
const config = Config.get('test');
|
||||
const androidDeviceRequest = {
|
||||
installationId: '12345678-abcd-abcd-abcd-123456789abc',
|
||||
deviceType: 'android',
|
||||
it('query installations with limit = 0', (done) => {
|
||||
var config = new Config('test');
|
||||
var androidDeviceRequest = {
|
||||
'installationId': '12345678-abcd-abcd-abcd-123456789abc',
|
||||
'deviceType': 'android'
|
||||
};
|
||||
const iosDeviceRequest = {
|
||||
installationId: '12345678-abcd-abcd-abcd-123456789abd',
|
||||
deviceType: 'ios',
|
||||
var iosDeviceRequest = {
|
||||
'installationId': '12345678-abcd-abcd-abcd-123456789abd',
|
||||
'deviceType': 'ios'
|
||||
};
|
||||
const request = {
|
||||
var request = {
|
||||
config: config,
|
||||
auth: auth.master(config),
|
||||
body: {},
|
||||
query: {
|
||||
limit: 0
|
||||
},
|
||||
info: {}
|
||||
};
|
||||
|
||||
new Config('test');
|
||||
var router = new InstallationsRouter();
|
||||
rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest)
|
||||
.then(() => {
|
||||
return rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest);
|
||||
}).then(() => {
|
||||
return router.handleFind(request);
|
||||
}).then((res) => {
|
||||
var response = res.response;
|
||||
expect(response.results.length).toEqual(0);
|
||||
done();
|
||||
}).catch((err) => {
|
||||
fail(JSON.stringify(err));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('query installations with count = 1', done => {
|
||||
var config = new Config('test');
|
||||
var androidDeviceRequest = {
|
||||
'installationId': '12345678-abcd-abcd-abcd-123456789abc',
|
||||
'deviceType': 'android'
|
||||
};
|
||||
var iosDeviceRequest = {
|
||||
'installationId': '12345678-abcd-abcd-abcd-123456789abd',
|
||||
'deviceType': 'ios'
|
||||
};
|
||||
var request = {
|
||||
config: config,
|
||||
auth: auth.master(config),
|
||||
body: {},
|
||||
query: {
|
||||
count: 1
|
||||
},
|
||||
info: {}
|
||||
};
|
||||
|
||||
var router = new InstallationsRouter();
|
||||
rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest)
|
||||
.then(() => rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest))
|
||||
.then(() => router.handleFind(request))
|
||||
.then((res) => {
|
||||
var response = res.response;
|
||||
expect(response.results.length).toEqual(2);
|
||||
expect(response.count).toEqual(2);
|
||||
done();
|
||||
})
|
||||
.catch(error => {
|
||||
fail(JSON.stringify(error));
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('query installations with limit = 0 and count = 1', (done) => {
|
||||
var config = new Config('test');
|
||||
var androidDeviceRequest = {
|
||||
'installationId': '12345678-abcd-abcd-abcd-123456789abc',
|
||||
'deviceType': 'android'
|
||||
};
|
||||
var iosDeviceRequest = {
|
||||
'installationId': '12345678-abcd-abcd-abcd-123456789abd',
|
||||
'deviceType': 'ios'
|
||||
};
|
||||
var request = {
|
||||
config: config,
|
||||
auth: auth.master(config),
|
||||
body: {},
|
||||
query: {
|
||||
limit: 0,
|
||||
count: 1
|
||||
},
|
||||
info: {},
|
||||
info: {}
|
||||
};
|
||||
|
||||
Config.get('test');
|
||||
const router = new InstallationsRouter();
|
||||
rest
|
||||
.create(config, auth.nobody(config), '_Installation', androidDeviceRequest)
|
||||
.then(() => {
|
||||
return rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest);
|
||||
})
|
||||
.then(() => {
|
||||
return router.handleFind(request);
|
||||
})
|
||||
.then(res => {
|
||||
const response = res.response;
|
||||
expect(response.results.length).toEqual(0);
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
fail(JSON.stringify(err));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('query installations with count = 1', done => {
|
||||
const config = Config.get('test');
|
||||
const androidDeviceRequest = {
|
||||
installationId: '12345678-abcd-abcd-abcd-123456789abc',
|
||||
deviceType: 'android',
|
||||
};
|
||||
const iosDeviceRequest = {
|
||||
installationId: '12345678-abcd-abcd-abcd-123456789abd',
|
||||
deviceType: 'ios',
|
||||
};
|
||||
const request = {
|
||||
config: config,
|
||||
auth: auth.master(config),
|
||||
body: {},
|
||||
query: {
|
||||
count: 1,
|
||||
},
|
||||
info: {},
|
||||
};
|
||||
|
||||
const router = new InstallationsRouter();
|
||||
rest
|
||||
.create(config, auth.nobody(config), '_Installation', androidDeviceRequest)
|
||||
.then(() => rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest))
|
||||
.then(() => router.handleFind(request))
|
||||
.then(res => {
|
||||
const response = res.response;
|
||||
expect(response.results.length).toEqual(2);
|
||||
expect(response.count).toEqual(2);
|
||||
done();
|
||||
})
|
||||
.catch(error => {
|
||||
fail(JSON.stringify(error));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it_only_db('postgres')('query installations with count = 1', async () => {
|
||||
const config = Config.get('test');
|
||||
const androidDeviceRequest = {
|
||||
installationId: '12345678-abcd-abcd-abcd-123456789abc',
|
||||
deviceType: 'android',
|
||||
};
|
||||
const iosDeviceRequest = {
|
||||
installationId: '12345678-abcd-abcd-abcd-123456789abd',
|
||||
deviceType: 'ios',
|
||||
};
|
||||
const request = {
|
||||
config: config,
|
||||
auth: auth.master(config),
|
||||
body: {},
|
||||
query: {
|
||||
count: 1,
|
||||
},
|
||||
info: {},
|
||||
};
|
||||
|
||||
const router = new InstallationsRouter();
|
||||
await rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest);
|
||||
await rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest);
|
||||
let res = await router.handleFind(request);
|
||||
let response = res.response;
|
||||
expect(response.results.length).toEqual(2);
|
||||
expect(response.count).toEqual(0); // estimate count is zero
|
||||
|
||||
const pgAdapter = config.database.adapter;
|
||||
await pgAdapter.updateEstimatedCount('_Installation');
|
||||
|
||||
res = await router.handleFind(request);
|
||||
response = res.response;
|
||||
expect(response.results.length).toEqual(2);
|
||||
expect(response.count).toEqual(2);
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('query installations with limit = 0 and count = 1', done => {
|
||||
const config = Config.get('test');
|
||||
const androidDeviceRequest = {
|
||||
installationId: '12345678-abcd-abcd-abcd-123456789abc',
|
||||
deviceType: 'android',
|
||||
};
|
||||
const iosDeviceRequest = {
|
||||
installationId: '12345678-abcd-abcd-abcd-123456789abd',
|
||||
deviceType: 'ios',
|
||||
};
|
||||
const request = {
|
||||
config: config,
|
||||
auth: auth.master(config),
|
||||
body: {},
|
||||
query: {
|
||||
limit: 0,
|
||||
count: 1,
|
||||
},
|
||||
info: {},
|
||||
};
|
||||
|
||||
const router = new InstallationsRouter();
|
||||
rest
|
||||
.create(config, auth.nobody(config), '_Installation', androidDeviceRequest)
|
||||
.then(() => {
|
||||
return rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest);
|
||||
})
|
||||
.then(() => {
|
||||
return router.handleFind(request);
|
||||
})
|
||||
.then(res => {
|
||||
const response = res.response;
|
||||
expect(response.results.length).toEqual(0);
|
||||
expect(response.count).toEqual(2);
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
fail(JSON.stringify(err));
|
||||
done();
|
||||
});
|
||||
var router = new InstallationsRouter();
|
||||
rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest)
|
||||
.then(() => {
|
||||
return rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest);
|
||||
}).then(() => {
|
||||
return router.handleFind(request);
|
||||
}).then((res) => {
|
||||
var response = res.response;
|
||||
expect(response.results.length).toEqual(0);
|
||||
expect(response.count).toEqual(2);
|
||||
done();
|
||||
}).catch((err) => {
|
||||
fail(JSON.stringify(err));
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,272 +0,0 @@
|
||||
const request = require('../lib/request');
|
||||
|
||||
const defaultHeaders = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Rest-API-Key': 'rest',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
const masterKeyHeaders = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Rest-API-Key': 'rest',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
const defaultOptions = {
|
||||
headers: defaultHeaders,
|
||||
json: true,
|
||||
};
|
||||
const masterKeyOptions = {
|
||||
headers: masterKeyHeaders,
|
||||
json: true,
|
||||
};
|
||||
|
||||
describe('JobSchedule', () => {
|
||||
it('should create _JobSchedule with masterKey', done => {
|
||||
const jobSchedule = new Parse.Object('_JobSchedule');
|
||||
jobSchedule.set({
|
||||
jobName: 'MY Cool Job',
|
||||
});
|
||||
jobSchedule
|
||||
.save(null, { useMasterKey: true })
|
||||
.then(() => {
|
||||
done();
|
||||
})
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('should fail creating _JobSchedule without masterKey', done => {
|
||||
const jobSchedule = new Parse.Object('_JobSchedule');
|
||||
jobSchedule.set({
|
||||
jobName: 'SomeJob',
|
||||
});
|
||||
jobSchedule
|
||||
.save(null)
|
||||
.then(done.fail)
|
||||
.catch(() => done());
|
||||
});
|
||||
|
||||
it('should reject access when not using masterKey (/jobs)', done => {
|
||||
request(
|
||||
Object.assign({ url: Parse.serverURL + '/cloud_code/jobs' }, defaultOptions)
|
||||
).then(done.fail, () => done());
|
||||
});
|
||||
|
||||
it('should reject access when not using masterKey (/jobs/data)', done => {
|
||||
request(
|
||||
Object.assign({ url: Parse.serverURL + '/cloud_code/jobs/data' }, defaultOptions)
|
||||
).then(done.fail, () => done());
|
||||
});
|
||||
|
||||
it('should reject access when not using masterKey (PUT /jobs/id)', done => {
|
||||
request(
|
||||
Object.assign(
|
||||
{ method: 'PUT', url: Parse.serverURL + '/cloud_code/jobs/jobId' },
|
||||
defaultOptions
|
||||
)
|
||||
).then(done.fail, () => done());
|
||||
});
|
||||
|
||||
it('should reject access when not using masterKey (DELETE /jobs/id)', done => {
|
||||
request(
|
||||
Object.assign(
|
||||
{ method: 'DELETE', url: Parse.serverURL + '/cloud_code/jobs/jobId' },
|
||||
defaultOptions
|
||||
)
|
||||
).then(done.fail, () => done());
|
||||
});
|
||||
|
||||
it('should allow access when using masterKey (GET /jobs)', done => {
|
||||
request(Object.assign({ url: Parse.serverURL + '/cloud_code/jobs' }, masterKeyOptions)).then(
|
||||
done,
|
||||
done.fail
|
||||
);
|
||||
});
|
||||
|
||||
it('should create a job schedule', done => {
|
||||
Parse.Cloud.job('job', () => {});
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
method: 'POST',
|
||||
url: Parse.serverURL + '/cloud_code/jobs',
|
||||
body: {
|
||||
job_schedule: {
|
||||
jobName: 'job',
|
||||
},
|
||||
},
|
||||
});
|
||||
request(options)
|
||||
.then(res => {
|
||||
expect(res.data.objectId).not.toBeUndefined();
|
||||
})
|
||||
.then(() => {
|
||||
return request(
|
||||
Object.assign({ url: Parse.serverURL + '/cloud_code/jobs' }, masterKeyOptions)
|
||||
);
|
||||
})
|
||||
.then(res => {
|
||||
expect(res.data.length).toBe(1);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('should fail creating a job with an invalid name', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
url: Parse.serverURL + '/cloud_code/jobs',
|
||||
method: 'POST',
|
||||
body: {
|
||||
job_schedule: {
|
||||
jobName: 'job',
|
||||
},
|
||||
},
|
||||
});
|
||||
request(options)
|
||||
.then(done.fail)
|
||||
.catch(() => done());
|
||||
});
|
||||
|
||||
it('should update a job', done => {
|
||||
Parse.Cloud.job('job1', () => {});
|
||||
Parse.Cloud.job('job2', () => {});
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
method: 'POST',
|
||||
url: Parse.serverURL + '/cloud_code/jobs',
|
||||
body: {
|
||||
job_schedule: {
|
||||
jobName: 'job1',
|
||||
},
|
||||
},
|
||||
});
|
||||
request(options)
|
||||
.then(res => {
|
||||
expect(res.data.objectId).not.toBeUndefined();
|
||||
return request(
|
||||
Object.assign(options, {
|
||||
url: Parse.serverURL + '/cloud_code/jobs/' + res.data.objectId,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
job_schedule: {
|
||||
jobName: 'job2',
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
return request(
|
||||
Object.assign({}, masterKeyOptions, {
|
||||
url: Parse.serverURL + '/cloud_code/jobs',
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(res => {
|
||||
expect(res.data.length).toBe(1);
|
||||
expect(res.data[0].jobName).toBe('job2');
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('should fail updating a job with an invalid name', done => {
|
||||
Parse.Cloud.job('job1', () => {});
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
method: 'POST',
|
||||
url: Parse.serverURL + '/cloud_code/jobs',
|
||||
body: {
|
||||
job_schedule: {
|
||||
jobName: 'job1',
|
||||
},
|
||||
},
|
||||
});
|
||||
request(options)
|
||||
.then(res => {
|
||||
expect(res.data.objectId).not.toBeUndefined();
|
||||
return request(
|
||||
Object.assign(options, {
|
||||
method: 'PUT',
|
||||
url: Parse.serverURL + '/cloud_code/jobs/' + res.data.objectId,
|
||||
body: {
|
||||
job_schedule: {
|
||||
jobName: 'job2',
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(done.fail)
|
||||
.catch(() => done());
|
||||
});
|
||||
|
||||
it('should destroy a job', done => {
|
||||
Parse.Cloud.job('job', () => {});
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
method: 'POST',
|
||||
url: Parse.serverURL + '/cloud_code/jobs',
|
||||
body: {
|
||||
job_schedule: {
|
||||
jobName: 'job',
|
||||
},
|
||||
},
|
||||
});
|
||||
request(options)
|
||||
.then(res => {
|
||||
expect(res.data.objectId).not.toBeUndefined();
|
||||
return request(
|
||||
Object.assign(
|
||||
{
|
||||
method: 'DELETE',
|
||||
url: Parse.serverURL + '/cloud_code/jobs/' + res.data.objectId,
|
||||
},
|
||||
masterKeyOptions
|
||||
)
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
return request(
|
||||
Object.assign(
|
||||
{
|
||||
url: Parse.serverURL + '/cloud_code/jobs',
|
||||
},
|
||||
masterKeyOptions
|
||||
)
|
||||
);
|
||||
})
|
||||
.then(res => {
|
||||
expect(res.data.length).toBe(0);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('should properly return job data', done => {
|
||||
Parse.Cloud.job('job1', () => {});
|
||||
Parse.Cloud.job('job2', () => {});
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
method: 'POST',
|
||||
url: Parse.serverURL + '/cloud_code/jobs',
|
||||
body: {
|
||||
job_schedule: {
|
||||
jobName: 'job1',
|
||||
},
|
||||
},
|
||||
});
|
||||
request(options)
|
||||
.then(response => {
|
||||
const res = response.data;
|
||||
expect(res.objectId).not.toBeUndefined();
|
||||
})
|
||||
.then(() => {
|
||||
return request(
|
||||
Object.assign({ url: Parse.serverURL + '/cloud_code/jobs/data' }, masterKeyOptions)
|
||||
);
|
||||
})
|
||||
.then(response => {
|
||||
const res = response.data;
|
||||
expect(res.in_use).toEqual(['job1']);
|
||||
expect(res.jobs).toContain('job1');
|
||||
expect(res.jobs).toContain('job2');
|
||||
expect(res.jobs.length).toBe(2);
|
||||
})
|
||||
.then(done)
|
||||
.catch(e => done.fail(e.data));
|
||||
});
|
||||
});
|
||||
@@ -1,259 +0,0 @@
|
||||
const ldap = require('../lib/Adapters/Auth/ldap');
|
||||
const mockLdapServer = require('./MockLdapServer');
|
||||
const fs = require('fs');
|
||||
const port = 12345;
|
||||
const sslport = 12346;
|
||||
|
||||
it('Should fail with missing options', done => {
|
||||
ldap
|
||||
.validateAuthData({ id: 'testuser', password: 'testpw' })
|
||||
.then(done.fail)
|
||||
.catch(err => {
|
||||
jequal(err.message, 'LDAP auth configuration missing');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Should return a resolved promise when validating the app id', done => {
|
||||
ldap.validateAppId().then(done).catch(done.fail);
|
||||
});
|
||||
|
||||
it('Should succeed with right credentials', done => {
|
||||
mockLdapServer(port, 'uid=testuser, o=example').then(server => {
|
||||
const options = {
|
||||
suffix: 'o=example',
|
||||
url: `ldap://localhost:${port}`,
|
||||
dn: 'uid={{id}}, o=example',
|
||||
};
|
||||
ldap
|
||||
.validateAuthData({ id: 'testuser', password: 'secret' }, options)
|
||||
.then(done)
|
||||
.catch(done.fail)
|
||||
.finally(() => server.close());
|
||||
});
|
||||
});
|
||||
|
||||
it('Should succeed with right credentials when LDAPS is used and certifcate is not checked', done => {
|
||||
mockLdapServer(sslport, 'uid=testuser, o=example', false, true).then(server => {
|
||||
const options = {
|
||||
suffix: 'o=example',
|
||||
url: `ldaps://localhost:${sslport}`,
|
||||
dn: 'uid={{id}}, o=example',
|
||||
tlsOptions: { rejectUnauthorized: false },
|
||||
};
|
||||
ldap
|
||||
.validateAuthData({ id: 'testuser', password: 'secret' }, options)
|
||||
.then(done)
|
||||
.catch(done.fail)
|
||||
.finally(() => server.close());
|
||||
});
|
||||
});
|
||||
|
||||
it('Should succeed when LDAPS is used and the presented certificate is the expected certificate', done => {
|
||||
mockLdapServer(sslport, 'uid=testuser, o=example', false, true).then(server => {
|
||||
const options = {
|
||||
suffix: 'o=example',
|
||||
url: `ldaps://localhost:${sslport}`,
|
||||
dn: 'uid={{id}}, o=example',
|
||||
tlsOptions: {
|
||||
ca: fs.readFileSync(__dirname + '/support/cert/cert.pem'),
|
||||
rejectUnauthorized: true,
|
||||
},
|
||||
};
|
||||
ldap
|
||||
.validateAuthData({ id: 'testuser', password: 'secret' }, options)
|
||||
.then(done)
|
||||
.catch(done.fail)
|
||||
.finally(() => server.close());
|
||||
});
|
||||
});
|
||||
|
||||
it('Should fail when LDAPS is used and the presented certificate is not the expected certificate', done => {
|
||||
mockLdapServer(sslport, 'uid=testuser, o=example', false, true).then(server => {
|
||||
const options = {
|
||||
suffix: 'o=example',
|
||||
url: `ldaps://localhost:${sslport}`,
|
||||
dn: 'uid={{id}}, o=example',
|
||||
tlsOptions: {
|
||||
ca: fs.readFileSync(__dirname + '/support/cert/anothercert.pem'),
|
||||
rejectUnauthorized: true,
|
||||
},
|
||||
};
|
||||
ldap
|
||||
.validateAuthData({ id: 'testuser', password: 'secret' }, options)
|
||||
.then(done.fail)
|
||||
.catch(err => {
|
||||
jequal(err.message, 'LDAPS: Certificate mismatch');
|
||||
done();
|
||||
})
|
||||
.finally(() => server.close());
|
||||
});
|
||||
});
|
||||
|
||||
it('Should fail when LDAPS is used certifcate matches but credentials are wrong', done => {
|
||||
mockLdapServer(sslport, 'uid=testuser, o=example', false, true).then(server => {
|
||||
const options = {
|
||||
suffix: 'o=example',
|
||||
url: `ldaps://localhost:${sslport}`,
|
||||
dn: 'uid={{id}}, o=example',
|
||||
tlsOptions: {
|
||||
ca: fs.readFileSync(__dirname + '/support/cert/cert.pem'),
|
||||
rejectUnauthorized: true,
|
||||
},
|
||||
};
|
||||
ldap
|
||||
.validateAuthData({ id: 'testuser', password: 'wrong!' }, options)
|
||||
.then(done.fail)
|
||||
.catch(err => {
|
||||
jequal(err.message, 'LDAP: Wrong username or password');
|
||||
done();
|
||||
})
|
||||
.finally(() => server.close());
|
||||
});
|
||||
});
|
||||
|
||||
it('Should fail with wrong credentials', done => {
|
||||
mockLdapServer(port, 'uid=testuser, o=example').then(server => {
|
||||
const options = {
|
||||
suffix: 'o=example',
|
||||
url: `ldap://localhost:${port}`,
|
||||
dn: 'uid={{id}}, o=example',
|
||||
};
|
||||
ldap
|
||||
.validateAuthData({ id: 'testuser', password: 'wrong!' }, options)
|
||||
.then(done.fail)
|
||||
.catch(err => {
|
||||
jequal(err.message, 'LDAP: Wrong username or password');
|
||||
done();
|
||||
})
|
||||
.finally(() => server.close());
|
||||
});
|
||||
});
|
||||
|
||||
it('Should succeed if user is in given group', done => {
|
||||
mockLdapServer(port, 'uid=testuser, o=example').then(server => {
|
||||
const options = {
|
||||
suffix: 'o=example',
|
||||
url: `ldap://localhost:${port}`,
|
||||
dn: 'uid={{id}}, o=example',
|
||||
groupCn: 'powerusers',
|
||||
groupFilter: '(&(uniqueMember=uid={{id}}, o=example)(objectClass=groupOfUniqueNames))',
|
||||
};
|
||||
|
||||
ldap
|
||||
.validateAuthData({ id: 'testuser', password: 'secret' }, options)
|
||||
.then(done)
|
||||
.catch(done.fail)
|
||||
.finally(() => server.close());
|
||||
});
|
||||
});
|
||||
|
||||
it('Should fail if user is not in given group', done => {
|
||||
mockLdapServer(port, 'uid=testuser, o=example').then(server => {
|
||||
const options = {
|
||||
suffix: 'o=example',
|
||||
url: `ldap://localhost:${port}`,
|
||||
dn: 'uid={{id}}, o=example',
|
||||
groupCn: 'groupTheUserIsNotIn',
|
||||
groupFilter: '(&(uniqueMember=uid={{id}}, o=example)(objectClass=groupOfUniqueNames))',
|
||||
};
|
||||
|
||||
ldap
|
||||
.validateAuthData({ id: 'testuser', password: 'secret' }, options)
|
||||
.then(done.fail)
|
||||
.catch(err => {
|
||||
jequal(err.message, 'LDAP: User not in group');
|
||||
done();
|
||||
})
|
||||
.finally(() => server.close());
|
||||
});
|
||||
});
|
||||
|
||||
it('Should fail if the LDAP server does not allow searching inside the provided suffix', done => {
|
||||
mockLdapServer(port, 'uid=testuser, o=example').then(server => {
|
||||
const options = {
|
||||
suffix: 'o=invalid',
|
||||
url: `ldap://localhost:${port}`,
|
||||
dn: 'uid={{id}}, o=example',
|
||||
groupCn: 'powerusers',
|
||||
groupFilter: '(&(uniqueMember=uid={{id}}, o=example)(objectClass=groupOfUniqueNames))',
|
||||
};
|
||||
|
||||
ldap
|
||||
.validateAuthData({ id: 'testuser', password: 'secret' }, options)
|
||||
.then(done.fail)
|
||||
.catch(err => {
|
||||
jequal(err.message, 'LDAP group search failed');
|
||||
done();
|
||||
})
|
||||
.finally(() => server.close());
|
||||
});
|
||||
});
|
||||
|
||||
it('Should fail if the LDAP server encounters an error while searching', done => {
|
||||
mockLdapServer(port, 'uid=testuser, o=example', true).then(server => {
|
||||
const options = {
|
||||
suffix: 'o=example',
|
||||
url: `ldap://localhost:${port}`,
|
||||
dn: 'uid={{id}}, o=example',
|
||||
groupCn: 'powerusers',
|
||||
groupFilter: '(&(uniqueMember=uid={{id}}, o=example)(objectClass=groupOfUniqueNames))',
|
||||
};
|
||||
|
||||
ldap
|
||||
.validateAuthData({ id: 'testuser', password: 'secret' }, options)
|
||||
.then(done.fail)
|
||||
.catch(err => {
|
||||
jequal(err.message, 'LDAP group search failed');
|
||||
done();
|
||||
})
|
||||
.finally(() => server.close());
|
||||
});
|
||||
});
|
||||
|
||||
it('Should delete the password from authData after validation', done => {
|
||||
mockLdapServer(port, 'uid=testuser, o=example', true).then(server => {
|
||||
const options = {
|
||||
suffix: 'o=example',
|
||||
url: `ldap://localhost:${port}`,
|
||||
dn: 'uid={{id}}, o=example',
|
||||
};
|
||||
|
||||
const authData = { id: 'testuser', password: 'secret' };
|
||||
|
||||
ldap
|
||||
.validateAuthData(authData, options)
|
||||
.then(() => {
|
||||
expect(authData).toEqual({ id: 'testuser' });
|
||||
done();
|
||||
})
|
||||
.catch(done.fail)
|
||||
.finally(() => server.close());
|
||||
});
|
||||
});
|
||||
|
||||
it('Should not save the password in the user record after authentication', done => {
|
||||
mockLdapServer(port, 'uid=testuser, o=example', true).then(server => {
|
||||
const options = {
|
||||
suffix: 'o=example',
|
||||
url: `ldap://localhost:${port}`,
|
||||
dn: 'uid={{id}}, o=example',
|
||||
};
|
||||
reconfigureServer({ auth: { ldap: options } }).then(() => {
|
||||
const authData = { authData: { id: 'testuser', password: 'secret' } };
|
||||
Parse.User.logInWith('ldap', authData).then(returnedUser => {
|
||||
const query = new Parse.Query('User');
|
||||
query
|
||||
.equalTo('objectId', returnedUser.id)
|
||||
.first({ useMasterKey: true })
|
||||
.then(user => {
|
||||
expect(user.get('authData')).toEqual({ ldap: { id: 'testuser' } });
|
||||
expect(user.get('authData').ldap.password).toBeUndefined();
|
||||
done();
|
||||
})
|
||||
.catch(done.fail)
|
||||
.finally(() => server.close());
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+30
-62
@@ -1,95 +1,63 @@
|
||||
const logging = require('../lib/Adapters/Logger/WinstonLogger');
|
||||
const Transport = require('winston-transport');
|
||||
var logging = require('../src/Adapters/Logger/WinstonLogger');
|
||||
var winston = require('winston');
|
||||
|
||||
class TestTransport extends Transport {
|
||||
log(info, callback) {
|
||||
class TestTransport extends winston.Transport {
|
||||
log(level, msg, meta, callback) {
|
||||
callback(null, true);
|
||||
}
|
||||
}
|
||||
|
||||
describe('WinstonLogger', () => {
|
||||
describe('Logger', () => {
|
||||
it('should add transport', () => {
|
||||
const testTransport = new TestTransport();
|
||||
const testTransport = new (TestTransport)({
|
||||
name: 'test'
|
||||
});
|
||||
spyOn(testTransport, 'log');
|
||||
logging.addTransport(testTransport);
|
||||
expect(logging.logger.transports.length).toBe(4);
|
||||
expect(Object.keys(logging.logger.transports).length).toBe(4);
|
||||
logging.logger.info('hi');
|
||||
expect(testTransport.log).toHaveBeenCalled();
|
||||
logging.logger.error('error');
|
||||
expect(testTransport.log).toHaveBeenCalled();
|
||||
logging.removeTransport(testTransport);
|
||||
expect(logging.logger.transports.length).toBe(3);
|
||||
expect(Object.keys(logging.logger.transports).length).toBe(3);
|
||||
});
|
||||
|
||||
it('should have files transports', done => {
|
||||
it('should have files transports', (done) => {
|
||||
reconfigureServer().then(() => {
|
||||
const transports = logging.logger.transports;
|
||||
expect(transports.length).toBe(3);
|
||||
const transportKeys = Object.keys(transports);
|
||||
expect(transportKeys.length).toBe(3);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should disable files logs', done => {
|
||||
it('should disable files logs', (done) => {
|
||||
reconfigureServer({
|
||||
logsFolder: null,
|
||||
logsFolder: null
|
||||
}).then(() => {
|
||||
const transports = logging.logger.transports;
|
||||
expect(transports.length).toBe(1);
|
||||
const transportKeys = Object.keys(transports);
|
||||
expect(transportKeys.length).toBe(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should have a timestamp', done => {
|
||||
logging.logger.info('hi');
|
||||
logging.logger.query({ limit: 1 }, (err, results) => {
|
||||
if (err) {
|
||||
done.fail(err);
|
||||
}
|
||||
expect(results['parse-server'][0].timestamp).toBeDefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('console should not be json', done => {
|
||||
// Force console transport
|
||||
reconfigureServer({
|
||||
logsFolder: null,
|
||||
silent: false,
|
||||
})
|
||||
.then(() => {
|
||||
spyOn(process.stdout, 'write');
|
||||
logging.logger.info('hi', { key: 'value' });
|
||||
expect(process.stdout.write).toHaveBeenCalled();
|
||||
const firstLog = process.stdout.write.calls.first().args[0];
|
||||
expect(firstLog).toEqual('info: hi {"key":"value"}' + '\n');
|
||||
return reconfigureServer();
|
||||
})
|
||||
.then(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should enable JSON logs', done => {
|
||||
it('should enable JSON logs', (done) => {
|
||||
// Force console transport
|
||||
reconfigureServer({
|
||||
logsFolder: null,
|
||||
jsonLogs: true,
|
||||
silent: false,
|
||||
})
|
||||
.then(() => {
|
||||
spyOn(process.stdout, 'write');
|
||||
logging.logger.info('hi', { key: 'value' });
|
||||
expect(process.stdout.write).toHaveBeenCalled();
|
||||
const firstLog = process.stdout.write.calls.first().args[0];
|
||||
expect(firstLog).toEqual(
|
||||
JSON.stringify({ key: 'value', level: 'info', message: 'hi' }) + '\n'
|
||||
);
|
||||
return reconfigureServer({
|
||||
jsonLogs: false,
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
done();
|
||||
silent: false
|
||||
}).then(() => {
|
||||
spyOn(process.stdout, 'write');
|
||||
logging.logger.info('hi', {key: 'value'});
|
||||
expect(process.stdout.write).toHaveBeenCalled();
|
||||
var firstLog = process.stdout.write.calls.first().args[0];
|
||||
expect(firstLog).toEqual(JSON.stringify({key: 'value', level: 'info', message: 'hi' })+'\n');
|
||||
return reconfigureServer({
|
||||
jsonLogs: false
|
||||
});
|
||||
}).then(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+38
-111
@@ -1,39 +1,35 @@
|
||||
const LoggerController = require('../lib/Controllers/LoggerController').LoggerController;
|
||||
const WinstonLoggerAdapter = require('../lib/Adapters/Logger/WinstonLoggerAdapter')
|
||||
.WinstonLoggerAdapter;
|
||||
var LoggerController = require('../src/Controllers/LoggerController').LoggerController;
|
||||
var WinstonLoggerAdapter = require('../src/Adapters/Logger/WinstonLoggerAdapter').WinstonLoggerAdapter;
|
||||
|
||||
describe('LoggerController', () => {
|
||||
it('can process an empty query without throwing', done => {
|
||||
it('can check process a query without throwing', (done) => {
|
||||
// Make mock request
|
||||
const query = {};
|
||||
var query = {};
|
||||
|
||||
const loggerController = new LoggerController(new WinstonLoggerAdapter());
|
||||
var loggerController = new LoggerController(new WinstonLoggerAdapter());
|
||||
|
||||
expect(() => {
|
||||
loggerController
|
||||
.getLogs(query)
|
||||
.then(function (res) {
|
||||
expect(res.length).not.toBe(0);
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
jfail(err);
|
||||
done();
|
||||
});
|
||||
loggerController.getLogs(query).then(function(res) {
|
||||
expect(res.length).not.toBe(0);
|
||||
done();
|
||||
}).catch((err) => {
|
||||
jfail(err);
|
||||
done();
|
||||
})
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('properly validates dateTimes', done => {
|
||||
it('properly validates dateTimes', (done) => {
|
||||
expect(LoggerController.validDateTime()).toBe(null);
|
||||
expect(LoggerController.validDateTime('String')).toBe(null);
|
||||
expect(LoggerController.validDateTime("String")).toBe(null);
|
||||
expect(LoggerController.validDateTime(123456).getTime()).toBe(123456);
|
||||
expect(LoggerController.validDateTime('2016-01-01Z00:00:00').getTime()).toBe(1451606400000);
|
||||
expect(LoggerController.validDateTime("2016-01-01Z00:00:00").getTime()).toBe(1451606400000);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can set the proper default values', done => {
|
||||
it('can set the proper default values', (done) => {
|
||||
// Make mock request
|
||||
const result = LoggerController.parseOptions();
|
||||
var result = LoggerController.parseOptions();
|
||||
expect(result.size).toEqual(10);
|
||||
expect(result.order).toEqual('desc');
|
||||
expect(result.level).toEqual('info');
|
||||
@@ -41,17 +37,17 @@ describe('LoggerController', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('can parse an ascending query without throwing', done => {
|
||||
it('can process a query without throwing', (done) => {
|
||||
// Make mock request
|
||||
const query = {
|
||||
from: '2016-01-01Z00:00:00',
|
||||
until: '2016-01-01Z00:00:00',
|
||||
var query = {
|
||||
from: "2016-01-01Z00:00:00",
|
||||
until: "2016-01-01Z00:00:00",
|
||||
size: 5,
|
||||
order: 'asc',
|
||||
level: 'error',
|
||||
level: 'error'
|
||||
};
|
||||
|
||||
const result = LoggerController.parseOptions(query);
|
||||
var result = LoggerController.parseOptions(query);
|
||||
|
||||
expect(result.from.getTime()).toEqual(1451606400000);
|
||||
expect(result.until.getTime()).toEqual(1451606400000);
|
||||
@@ -62,103 +58,34 @@ describe('LoggerController', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('can process an ascending query without throwing', done => {
|
||||
const query = {
|
||||
size: 5,
|
||||
order: 'asc',
|
||||
level: 'error',
|
||||
};
|
||||
|
||||
const loggerController = new LoggerController(new WinstonLoggerAdapter());
|
||||
|
||||
expect(() => {
|
||||
loggerController
|
||||
.getLogs(query)
|
||||
.then(function (res) {
|
||||
expect(res.length).not.toBe(0);
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
jfail(err);
|
||||
fail('should not fail');
|
||||
done();
|
||||
});
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('can parse a descending query without throwing', done => {
|
||||
it('can check process a query without throwing', (done) => {
|
||||
// Make mock request
|
||||
const query = {
|
||||
from: '2016-01-01Z00:00:00',
|
||||
until: '2016-01-01Z00:00:00',
|
||||
var query = {
|
||||
from: "2016-01-01",
|
||||
until: "2016-01-30",
|
||||
size: 5,
|
||||
order: 'desc',
|
||||
level: 'error',
|
||||
level: 'error'
|
||||
};
|
||||
|
||||
const result = LoggerController.parseOptions(query);
|
||||
|
||||
expect(result.from.getTime()).toEqual(1451606400000);
|
||||
expect(result.until.getTime()).toEqual(1451606400000);
|
||||
expect(result.size).toEqual(5);
|
||||
expect(result.order).toEqual('desc');
|
||||
expect(result.level).toEqual('error');
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('can process a descending query without throwing', done => {
|
||||
const query = {
|
||||
size: 5,
|
||||
order: 'desc',
|
||||
level: 'error',
|
||||
};
|
||||
|
||||
const loggerController = new LoggerController(new WinstonLoggerAdapter());
|
||||
var loggerController = new LoggerController(new WinstonLoggerAdapter());
|
||||
|
||||
expect(() => {
|
||||
loggerController
|
||||
.getLogs(query)
|
||||
.then(function (res) {
|
||||
expect(res.length).not.toBe(0);
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
jfail(err);
|
||||
fail('should not fail');
|
||||
done();
|
||||
});
|
||||
loggerController.getLogs(query).then(function(res) {
|
||||
expect(res.length).toBe(0);
|
||||
done();
|
||||
}).catch((err) => {
|
||||
jfail(err);
|
||||
fail("should not fail");
|
||||
done();
|
||||
})
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should throw without an adapter', done => {
|
||||
it('should throw without an adapter', (done) => {
|
||||
expect(() => {
|
||||
new LoggerController();
|
||||
}).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('should replace implementations with verbose', done => {
|
||||
const adapter = new WinstonLoggerAdapter();
|
||||
const logger = new LoggerController(adapter, null, { verbose: true });
|
||||
spyOn(adapter, 'log');
|
||||
logger.silly('yo!');
|
||||
expect(adapter.log).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
|
||||
it('should replace implementations with logLevel', done => {
|
||||
const adapter = new WinstonLoggerAdapter();
|
||||
const logger = new LoggerController(adapter, null, { logLevel: 'error' });
|
||||
spyOn(adapter, 'log');
|
||||
logger.warn('yo!');
|
||||
logger.info('yo!');
|
||||
logger.debug('yo!');
|
||||
logger.verbose('yo!');
|
||||
logger.silly('yo!');
|
||||
expect(adapter.log).not.toHaveBeenCalled();
|
||||
logger.error('error');
|
||||
expect(adapter.log).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
+22
-128
@@ -1,27 +1,26 @@
|
||||
'use strict';
|
||||
|
||||
const request = require('../lib/request');
|
||||
const LogsRouter = require('../lib/Routers/LogsRouter').LogsRouter;
|
||||
const LoggerController = require('../lib/Controllers/LoggerController').LoggerController;
|
||||
const WinstonLoggerAdapter = require('../lib/Adapters/Logger/WinstonLoggerAdapter')
|
||||
.WinstonLoggerAdapter;
|
||||
const request = require('request');
|
||||
var LogsRouter = require('../src/Routers/LogsRouter').LogsRouter;
|
||||
var LoggerController = require('../src/Controllers/LoggerController').LoggerController;
|
||||
var WinstonLoggerAdapter = require('../src/Adapters/Logger/WinstonLoggerAdapter').WinstonLoggerAdapter;
|
||||
|
||||
const loggerController = new LoggerController(new WinstonLoggerAdapter());
|
||||
|
||||
describe('LogsRouter', () => {
|
||||
it('can check valid master key of request', done => {
|
||||
it('can check valid master key of request', (done) => {
|
||||
// Make mock request
|
||||
const request = {
|
||||
var request = {
|
||||
auth: {
|
||||
isMaster: true,
|
||||
isMaster: true
|
||||
},
|
||||
query: {},
|
||||
config: {
|
||||
loggerController: loggerController,
|
||||
},
|
||||
loggerController: loggerController
|
||||
}
|
||||
};
|
||||
|
||||
const router = new LogsRouter();
|
||||
var router = new LogsRouter();
|
||||
|
||||
expect(() => {
|
||||
router.validateRequest(request);
|
||||
@@ -29,19 +28,19 @@ describe('LogsRouter', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('can check invalid construction of controller', done => {
|
||||
it('can check invalid construction of controller', (done) => {
|
||||
// Make mock request
|
||||
const request = {
|
||||
var request = {
|
||||
auth: {
|
||||
isMaster: true,
|
||||
isMaster: true
|
||||
},
|
||||
query: {},
|
||||
config: {
|
||||
loggerController: undefined, // missing controller
|
||||
},
|
||||
loggerController: undefined // missing controller
|
||||
}
|
||||
};
|
||||
|
||||
const router = new LogsRouter();
|
||||
var router = new LogsRouter();
|
||||
|
||||
expect(() => {
|
||||
router.validateRequest(request);
|
||||
@@ -50,122 +49,17 @@ describe('LogsRouter', () => {
|
||||
});
|
||||
|
||||
it('can check invalid master key of request', done => {
|
||||
request({
|
||||
request.get({
|
||||
url: 'http://localhost:8378/1/scriptlog',
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
}).then(fail, response => {
|
||||
const body = response.data;
|
||||
expect(response.status).toEqual(403);
|
||||
'X-Parse-REST-API-Key': 'rest'
|
||||
}
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(403);
|
||||
expect(body.error).toEqual('unauthorized: master key is required');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
const headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
};
|
||||
|
||||
/**
|
||||
* Verifies simple passwords in GET login requests with special characters are scrubbed from the verbose log
|
||||
*/
|
||||
it('does scrub simple passwords on GET login', done => {
|
||||
reconfigureServer({
|
||||
verbose: true,
|
||||
}).then(function () {
|
||||
request({
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/login?username=test&password=simplepass.com',
|
||||
})
|
||||
.catch(() => {})
|
||||
.then(() => {
|
||||
request({
|
||||
url: 'http://localhost:8378/1/scriptlog?size=4&level=verbose',
|
||||
headers: headers,
|
||||
}).then(response => {
|
||||
const body = response.data;
|
||||
expect(response.status).toEqual(200);
|
||||
// 4th entry is our actual GET request
|
||||
expect(body[2].url).toEqual('/1/login?username=test&password=********');
|
||||
expect(body[2].message).toEqual(
|
||||
'REQUEST for [GET] /1/login?username=test&password=********: {}'
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Verifies complex passwords in GET login requests with special characters are scrubbed from the verbose log
|
||||
*/
|
||||
it('does scrub complex passwords on GET login', done => {
|
||||
reconfigureServer({
|
||||
verbose: true,
|
||||
})
|
||||
.then(function () {
|
||||
return request({
|
||||
headers: headers,
|
||||
// using urlencoded password, 'simple @,/?:&=+$#pass.com'
|
||||
url:
|
||||
'http://localhost:8378/1/login?username=test&password=simple%20%40%2C%2F%3F%3A%26%3D%2B%24%23pass.com',
|
||||
})
|
||||
.catch(() => {})
|
||||
.then(() => {
|
||||
return request({
|
||||
url: 'http://localhost:8378/1/scriptlog?size=4&level=verbose',
|
||||
headers: headers,
|
||||
}).then(response => {
|
||||
const body = response.data;
|
||||
expect(response.status).toEqual(200);
|
||||
// 4th entry is our actual GET request
|
||||
expect(body[2].url).toEqual('/1/login?username=test&password=********');
|
||||
expect(body[2].message).toEqual(
|
||||
'REQUEST for [GET] /1/login?username=test&password=********: {}'
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
/**
|
||||
* Verifies fields in POST login requests are NOT present in the verbose log
|
||||
*/
|
||||
it('does not have password field in POST login', done => {
|
||||
reconfigureServer({
|
||||
verbose: true,
|
||||
}).then(function () {
|
||||
request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/login',
|
||||
body: {
|
||||
username: 'test',
|
||||
password: 'simplepass.com',
|
||||
},
|
||||
})
|
||||
.catch(() => {})
|
||||
.then(() => {
|
||||
request({
|
||||
url: 'http://localhost:8378/1/scriptlog?size=4&level=verbose',
|
||||
headers: headers,
|
||||
}).then(response => {
|
||||
const body = response.data;
|
||||
expect(response.status).toEqual(200);
|
||||
// 4th entry is our actual GET request
|
||||
expect(body[2].url).toEqual('/1/login');
|
||||
expect(body[2].message).toEqual(
|
||||
'REQUEST for [POST] /1/login: {\n "username": "test",\n "password": "********"\n}'
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+25
-297
@@ -1,20 +1,21 @@
|
||||
const middlewares = require('../lib/middlewares');
|
||||
const AppCache = require('../lib/cache').AppCache;
|
||||
var middlewares = require('../src/middlewares');
|
||||
var AppCache = require('../src/cache').AppCache;
|
||||
|
||||
describe('middlewares', () => {
|
||||
let fakeReq, fakeRes;
|
||||
|
||||
var fakeReq, fakeRes;
|
||||
|
||||
beforeEach(() => {
|
||||
fakeReq = {
|
||||
originalUrl: 'http://example.com/parse/',
|
||||
url: 'http://example.com/',
|
||||
body: {
|
||||
_ApplicationId: 'FakeAppId',
|
||||
_ApplicationId: 'FakeAppId'
|
||||
},
|
||||
headers: {},
|
||||
get: key => {
|
||||
return fakeReq.headers[key.toLowerCase()];
|
||||
},
|
||||
get: (key) => {
|
||||
return fakeReq.headers[key.toLowerCase()]
|
||||
}
|
||||
};
|
||||
fakeRes = jasmine.createSpyObj('fakeRes', ['end', 'status']);
|
||||
AppCache.put(fakeReq.body._ApplicationId, {});
|
||||
@@ -24,21 +25,21 @@ describe('middlewares', () => {
|
||||
AppCache.del(fakeReq.body._ApplicationId);
|
||||
});
|
||||
|
||||
it('should use _ContentType if provided', done => {
|
||||
it('should use _ContentType if provided', (done) => {
|
||||
expect(fakeReq.headers['content-type']).toEqual(undefined);
|
||||
const contentType = 'image/jpeg';
|
||||
var contentType = 'image/jpeg';
|
||||
fakeReq.body._ContentType = contentType;
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||
expect(fakeReq.headers['content-type']).toEqual(contentType);
|
||||
expect(fakeReq.body._ContentType).toEqual(undefined);
|
||||
done();
|
||||
done()
|
||||
});
|
||||
});
|
||||
|
||||
it('should give invalid response when keys are configured but no key supplied', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
restAPIKey: 'restAPIKey',
|
||||
restAPIKey: 'restAPIKey'
|
||||
});
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
expect(fakeRes.status).toHaveBeenCalledWith(403);
|
||||
@@ -47,7 +48,7 @@ describe('middlewares', () => {
|
||||
it('should give invalid response when keys are configured but supplied key is incorrect', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
restAPIKey: 'restAPIKey',
|
||||
restAPIKey: 'restAPIKey'
|
||||
});
|
||||
fakeReq.headers['x-parse-rest-api-key'] = 'wrongKey';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
@@ -57,18 +58,19 @@ describe('middlewares', () => {
|
||||
it('should give invalid response when keys are configured but different key is supplied', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
restAPIKey: 'restAPIKey',
|
||||
restAPIKey: 'restAPIKey'
|
||||
});
|
||||
fakeReq.headers['x-parse-client-key'] = 'clientKey';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
expect(fakeRes.status).toHaveBeenCalledWith(403);
|
||||
});
|
||||
|
||||
it('should succeed when any one of the configured keys supplied', done => {
|
||||
|
||||
it('should succeed when any one of the configured keys supplied', (done) => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
clientKey: 'clientKey',
|
||||
masterKey: 'masterKey',
|
||||
restAPIKey: 'restAPIKey',
|
||||
restAPIKey: 'restAPIKey'
|
||||
});
|
||||
fakeReq.headers['x-parse-rest-api-key'] = 'restAPIKey';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||
@@ -77,22 +79,9 @@ describe('middlewares', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should succeed when client key supplied but empty', done => {
|
||||
it('should succeed when no keys are configured and none supplied', (done) => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
clientKey: '',
|
||||
masterKey: 'masterKey',
|
||||
restAPIKey: 'restAPIKey',
|
||||
});
|
||||
fakeReq.headers['x-parse-client-key'] = '';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||
expect(fakeRes.status).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should succeed when no keys are configured and none supplied', done => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKey: 'masterKey'
|
||||
});
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||
expect(fakeRes.status).not.toHaveBeenCalled();
|
||||
@@ -105,27 +94,25 @@ describe('middlewares', () => {
|
||||
installationId: '_InstallationId',
|
||||
sessionToken: '_SessionToken',
|
||||
masterKey: '_MasterKey',
|
||||
javascriptKey: '_JavaScriptKey',
|
||||
javascriptKey: '_JavaScriptKey'
|
||||
};
|
||||
|
||||
const BodyKeys = Object.keys(BodyParams);
|
||||
|
||||
BodyKeys.forEach(infoKey => {
|
||||
BodyKeys.forEach((infoKey) => {
|
||||
const bodyKey = BodyParams[infoKey];
|
||||
const keyValue = 'Fake' + bodyKey;
|
||||
// javascriptKey is the only one that gets defaulted,
|
||||
const otherKeys = BodyKeys.filter(
|
||||
otherKey => otherKey !== infoKey && otherKey !== 'javascriptKey'
|
||||
);
|
||||
// javascriptKey is the only one that gets defaulted,
|
||||
const otherKeys = BodyKeys.filter((otherKey) => otherKey !== infoKey && otherKey !== 'javascriptKey');
|
||||
|
||||
it(`it should pull ${bodyKey} into req.info`, done => {
|
||||
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);
|
||||
|
||||
otherKeys.forEach(otherKey => {
|
||||
otherKeys.forEach((otherKey) => {
|
||||
expect(fakeReq.info[otherKey]).toEqual(undefined);
|
||||
});
|
||||
|
||||
@@ -133,263 +120,4 @@ describe('middlewares', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should not succeed if the ip does not belong to masterKeyIps list', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['ip1', 'ip2'],
|
||||
});
|
||||
fakeReq.ip = 'ip3';
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
expect(fakeRes.status).toHaveBeenCalledWith(403);
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not succeed if the connection.remoteAddress does not belong to masterKeyIps list', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['ip1', 'ip2'],
|
||||
});
|
||||
fakeReq.connection = { remoteAddress: 'ip3' };
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
expect(fakeRes.status).toHaveBeenCalledWith(403);
|
||||
});
|
||||
|
||||
it('should succeed if the connection.remoteAddress does belong to masterKeyIps list', done => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['ip1', 'ip2'],
|
||||
});
|
||||
fakeReq.connection = { remoteAddress: 'ip1' };
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||
expect(fakeRes.status).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not succeed if the socket.remoteAddress does not belong to masterKeyIps list', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['ip1', 'ip2'],
|
||||
});
|
||||
fakeReq.socket = { remoteAddress: 'ip3' };
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
expect(fakeRes.status).toHaveBeenCalledWith(403);
|
||||
});
|
||||
|
||||
it('should succeed if the socket.remoteAddress does belong to masterKeyIps list', done => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['ip1', 'ip2'],
|
||||
});
|
||||
fakeReq.socket = { remoteAddress: 'ip1' };
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||
expect(fakeRes.status).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not succeed if the connection.socket.remoteAddress does not belong to masterKeyIps list', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['ip1', 'ip2'],
|
||||
});
|
||||
fakeReq.connection = { socket: { remoteAddress: 'ip3' } };
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
expect(fakeRes.status).toHaveBeenCalledWith(403);
|
||||
});
|
||||
|
||||
it('should succeed if the connection.socket.remoteAddress does belong to masterKeyIps list', done => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['ip1', 'ip2'],
|
||||
});
|
||||
fakeReq.connection = { socket: { remoteAddress: 'ip1' } };
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||
expect(fakeRes.status).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow any ip to use masterKey if masterKeyIps is empty', done => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: [],
|
||||
});
|
||||
fakeReq.ip = 'ip1';
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||
expect(fakeRes.status).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should succeed if xff header does belong to masterKeyIps', done => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['ip1'],
|
||||
});
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
fakeReq.headers['x-forwarded-for'] = 'ip1, ip2, ip3';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||
expect(fakeRes.status).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should succeed if xff header with one ip does belong to masterKeyIps', done => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['ip1'],
|
||||
});
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
fakeReq.headers['x-forwarded-for'] = 'ip1';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||
expect(fakeRes.status).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not succeed if xff header does not belong to masterKeyIps', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['ip4'],
|
||||
});
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
fakeReq.headers['x-forwarded-for'] = 'ip1, ip2, ip3';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
expect(fakeRes.status).toHaveBeenCalledWith(403);
|
||||
});
|
||||
|
||||
it('should not succeed if xff header is empty and masterKeyIps is set', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['ip1'],
|
||||
});
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
fakeReq.headers['x-forwarded-for'] = '';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
expect(fakeRes.status).toHaveBeenCalledWith(403);
|
||||
});
|
||||
|
||||
it('should properly expose the headers', () => {
|
||||
const headers = {};
|
||||
const res = {
|
||||
header: (key, value) => {
|
||||
headers[key] = value;
|
||||
},
|
||||
};
|
||||
const allowCrossDomain = middlewares.allowCrossDomain(fakeReq.body._ApplicationId);
|
||||
allowCrossDomain(fakeReq, res, () => {});
|
||||
expect(Object.keys(headers).length).toBe(4);
|
||||
expect(headers['Access-Control-Expose-Headers']).toBe(
|
||||
'X-Parse-Job-Status-Id, X-Parse-Push-Status-Id'
|
||||
);
|
||||
});
|
||||
|
||||
it('should set default Access-Control-Allow-Headers if allowHeaders are empty', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
allowHeaders: undefined,
|
||||
});
|
||||
const headers = {};
|
||||
const res = {
|
||||
header: (key, value) => {
|
||||
headers[key] = value;
|
||||
},
|
||||
};
|
||||
const allowCrossDomain = middlewares.allowCrossDomain(fakeReq.body._ApplicationId);
|
||||
allowCrossDomain(fakeReq, res, () => {});
|
||||
expect(headers['Access-Control-Allow-Headers']).toContain(middlewares.DEFAULT_ALLOWED_HEADERS);
|
||||
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
allowHeaders: [],
|
||||
});
|
||||
allowCrossDomain(fakeReq, res, () => {});
|
||||
expect(headers['Access-Control-Allow-Headers']).toContain(middlewares.DEFAULT_ALLOWED_HEADERS);
|
||||
});
|
||||
|
||||
it('should append custom headers to Access-Control-Allow-Headers if allowHeaders provided', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
allowHeaders: ['Header-1', 'Header-2'],
|
||||
});
|
||||
const headers = {};
|
||||
const res = {
|
||||
header: (key, value) => {
|
||||
headers[key] = value;
|
||||
},
|
||||
};
|
||||
const allowCrossDomain = middlewares.allowCrossDomain(fakeReq.body._ApplicationId);
|
||||
allowCrossDomain(fakeReq, res, () => {});
|
||||
expect(headers['Access-Control-Allow-Headers']).toContain('Header-1, Header-2');
|
||||
expect(headers['Access-Control-Allow-Headers']).toContain(middlewares.DEFAULT_ALLOWED_HEADERS);
|
||||
});
|
||||
|
||||
it('should set default Access-Control-Allow-Origin if allowOrigin is empty', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
allowOrigin: undefined,
|
||||
});
|
||||
const headers = {};
|
||||
const res = {
|
||||
header: (key, value) => {
|
||||
headers[key] = value;
|
||||
},
|
||||
};
|
||||
const allowCrossDomain = middlewares.allowCrossDomain(fakeReq.body._ApplicationId);
|
||||
allowCrossDomain(fakeReq, res, () => {});
|
||||
expect(headers['Access-Control-Allow-Origin']).toEqual('*');
|
||||
});
|
||||
|
||||
it('should set custom origin to Access-Control-Allow-Origin if allowOrigin is provided', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
allowOrigin: 'https://parseplatform.org/',
|
||||
});
|
||||
const headers = {};
|
||||
const res = {
|
||||
header: (key, value) => {
|
||||
headers[key] = value;
|
||||
},
|
||||
};
|
||||
const allowCrossDomain = middlewares.allowCrossDomain(fakeReq.body._ApplicationId);
|
||||
allowCrossDomain(fakeReq, res, () => {});
|
||||
expect(headers['Access-Control-Allow-Origin']).toEqual('https://parseplatform.org/');
|
||||
});
|
||||
|
||||
it('should use user provided on field userFromJWT', done => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
});
|
||||
fakeReq.userFromJWT = 'fake-user';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||
expect(fakeReq.auth.user).toEqual('fake-user');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should give invalid response when upload file without x-parse-application-id in header', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
});
|
||||
fakeReq.body = Buffer.from('fake-file');
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
expect(fakeRes.status).toHaveBeenCalledWith(403);
|
||||
});
|
||||
});
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
module.exports = function (options) {
|
||||
module.exports = function(options) {
|
||||
return {
|
||||
options: options,
|
||||
options: options
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => Promise.resolve(),
|
||||
};
|
||||
sendMail: () => Promise.resolve()
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
module.exports = options => {
|
||||
if (!options) {
|
||||
throw 'Options were not provided';
|
||||
throw "Options were not provided"
|
||||
}
|
||||
const adapter = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => Promise.resolve(),
|
||||
sendMail: () => Promise.resolve()
|
||||
};
|
||||
if (options.sendMail) {
|
||||
adapter.sendMail = options.sendMail;
|
||||
adapter.sendMail = options.sendMail
|
||||
}
|
||||
if (options.sendPasswordResetEmail) {
|
||||
adapter.sendPasswordResetEmail = options.sendPasswordResetEmail;
|
||||
adapter.sendPasswordResetEmail = options.sendPasswordResetEmail
|
||||
}
|
||||
if (options.sendVerificationEmail) {
|
||||
adapter.sendVerificationEmail = options.sendVerificationEmail;
|
||||
}
|
||||
|
||||
return adapter;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
const ldapjs = require('ldapjs');
|
||||
const fs = require('fs');
|
||||
|
||||
const tlsOptions = {
|
||||
key: fs.readFileSync(__dirname + '/support/cert/key.pem'),
|
||||
certificate: fs.readFileSync(__dirname + '/support/cert/cert.pem'),
|
||||
};
|
||||
|
||||
function newServer(port, dn, provokeSearchError = false, ssl = false) {
|
||||
const server = ssl ? ldapjs.createServer(tlsOptions) : ldapjs.createServer();
|
||||
|
||||
server.bind('o=example', function (req, res, next) {
|
||||
if (req.dn.toString() !== dn || req.credentials !== 'secret')
|
||||
return next(new ldapjs.InvalidCredentialsError());
|
||||
res.end();
|
||||
return next();
|
||||
});
|
||||
|
||||
server.search('o=example', function (req, res, next) {
|
||||
if (provokeSearchError) {
|
||||
res.end(ldapjs.LDAP_SIZE_LIMIT_EXCEEDED);
|
||||
return next();
|
||||
}
|
||||
const obj = {
|
||||
dn: req.dn.toString(),
|
||||
attributes: {
|
||||
objectclass: ['organization', 'top'],
|
||||
o: 'example',
|
||||
},
|
||||
};
|
||||
|
||||
const group = {
|
||||
dn: req.dn.toString(),
|
||||
attributes: {
|
||||
objectClass: ['groupOfUniqueNames', 'top'],
|
||||
uniqueMember: ['uid=testuser, o=example'],
|
||||
cn: 'powerusers',
|
||||
ou: 'powerusers',
|
||||
},
|
||||
};
|
||||
|
||||
if (req.filter.matches(obj.attributes)) {
|
||||
res.send(obj);
|
||||
}
|
||||
|
||||
if (req.filter.matches(group.attributes)) {
|
||||
res.send(group);
|
||||
}
|
||||
res.end();
|
||||
});
|
||||
return new Promise(resolve => server.listen(port, () => resolve(server)));
|
||||
}
|
||||
|
||||
module.exports = newServer;
|
||||
@@ -1,9 +0,0 @@
|
||||
module.exports = function (options) {
|
||||
return {
|
||||
options: options,
|
||||
send: function () {},
|
||||
getValidPushTypes: function () {
|
||||
return Object.keys(options.options);
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,52 +1,43 @@
|
||||
'use strict';
|
||||
|
||||
const MongoSchemaCollection = require('../lib/Adapters/Storage/Mongo/MongoSchemaCollection')
|
||||
.default;
|
||||
const MongoSchemaCollection = require('../src/Adapters/Storage/Mongo/MongoSchemaCollection').default;
|
||||
|
||||
describe('MongoSchemaCollection', () => {
|
||||
it('can transform legacy _client_permissions keys to parse format', done => {
|
||||
expect(
|
||||
MongoSchemaCollection._TESTmongoSchemaToParseSchema({
|
||||
_id: '_Installation',
|
||||
_client_permissions: {
|
||||
get: true,
|
||||
find: true,
|
||||
count: true,
|
||||
update: true,
|
||||
create: true,
|
||||
delete: true,
|
||||
},
|
||||
_metadata: {
|
||||
class_permissions: {
|
||||
get: { '*': true },
|
||||
find: { '*': true },
|
||||
count: { '*': true },
|
||||
update: { '*': true },
|
||||
create: { '*': true },
|
||||
delete: { '*': true },
|
||||
addField: { '*': true },
|
||||
protectedFields: { '*': [] },
|
||||
},
|
||||
indexes: {
|
||||
name1: { deviceToken: 1 },
|
||||
},
|
||||
},
|
||||
installationId: 'string',
|
||||
deviceToken: 'string',
|
||||
deviceType: 'string',
|
||||
channels: 'array',
|
||||
user: '*_User',
|
||||
pushType: 'string',
|
||||
GCMSenderId: 'string',
|
||||
timeZone: 'string',
|
||||
localeIdentifier: 'string',
|
||||
badge: 'number',
|
||||
appVersion: 'string',
|
||||
appName: 'string',
|
||||
appIdentifier: 'string',
|
||||
parseVersion: 'string',
|
||||
})
|
||||
).toEqual({
|
||||
expect(MongoSchemaCollection._TESTmongoSchemaToParseSchema({
|
||||
"_id":"_Installation",
|
||||
"_client_permissions":{
|
||||
"get":true,
|
||||
"find":true,
|
||||
"update":true,
|
||||
"create":true,
|
||||
"delete":true,
|
||||
},
|
||||
"_metadata":{
|
||||
"class_permissions":{
|
||||
"get":{"*":true},
|
||||
"find":{"*":true},
|
||||
"update":{"*":true},
|
||||
"create":{"*":true},
|
||||
"delete":{"*":true},
|
||||
"addField":{"*":true},
|
||||
}
|
||||
},
|
||||
"installationId":"string",
|
||||
"deviceToken":"string",
|
||||
"deviceType":"string",
|
||||
"channels":"array",
|
||||
"user":"*_User",
|
||||
"pushType":"string",
|
||||
"GCMSenderId":"string",
|
||||
"timeZone":"string",
|
||||
"localeIdentifier":"string",
|
||||
"badge":"number",
|
||||
"appVersion":"string",
|
||||
"appName":"string",
|
||||
"appIdentifier":"string",
|
||||
"parseVersion":"string",
|
||||
})).toEqual({
|
||||
className: '_Installation',
|
||||
fields: {
|
||||
installationId: { type: 'String' },
|
||||
@@ -71,16 +62,11 @@ describe('MongoSchemaCollection', () => {
|
||||
classLevelPermissions: {
|
||||
find: { '*': true },
|
||||
get: { '*': true },
|
||||
count: { '*': true },
|
||||
create: { '*': true },
|
||||
update: { '*': true },
|
||||
delete: { '*': true },
|
||||
addField: { '*': true },
|
||||
protectedFields: { '*': [] },
|
||||
},
|
||||
indexes: {
|
||||
name1: { deviceToken: 1 },
|
||||
},
|
||||
}
|
||||
});
|
||||
done();
|
||||
});
|
||||
|
||||
+175
-488
@@ -1,29 +1,22 @@
|
||||
'use strict';
|
||||
|
||||
const MongoStorageAdapter = require('../lib/Adapters/Storage/Mongo/MongoStorageAdapter').default;
|
||||
const { MongoClient } = require('mongodb');
|
||||
const MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter');
|
||||
const MongoClient = require('mongodb').MongoClient;
|
||||
const databaseURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase';
|
||||
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 } },
|
||||
db: () => null,
|
||||
};
|
||||
|
||||
// These tests are specific to the mongo storage adapter + mongo storage format
|
||||
// and will eventually be moved into their own repo
|
||||
describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
beforeEach(done => {
|
||||
new MongoStorageAdapter({ uri: databaseURI }).deleteAllClasses().then(done, fail);
|
||||
new MongoStorageAdapter({ uri: databaseURI })
|
||||
.deleteAllClasses()
|
||||
.then(done, fail);
|
||||
});
|
||||
|
||||
it('auto-escapes symbols in auth information', () => {
|
||||
spyOn(MongoClient, 'connect').and.returnValue(Promise.resolve(fakeClient));
|
||||
spyOn(MongoClient, 'connect').and.returnValue(Promise.resolve(null));
|
||||
new MongoStorageAdapter({
|
||||
uri: 'mongodb://user!with@+ symbols:password!with@+ symbols@localhost:1234/parse',
|
||||
uri: 'mongodb://user!with@+ symbols:password!with@+ symbols@localhost:1234/parse'
|
||||
}).connect();
|
||||
expect(MongoClient.connect).toHaveBeenCalledWith(
|
||||
'mongodb://user!with%40%2B%20symbols:password!with%40%2B%20symbols@localhost:1234/parse',
|
||||
@@ -32,9 +25,9 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
});
|
||||
|
||||
it("doesn't double escape already URI-encoded information", () => {
|
||||
spyOn(MongoClient, 'connect').and.returnValue(Promise.resolve(fakeClient));
|
||||
spyOn(MongoClient, 'connect').and.returnValue(Promise.resolve(null));
|
||||
new MongoStorageAdapter({
|
||||
uri: 'mongodb://user!with%40%2B%20symbols:password!with%40%2B%20symbols@localhost:1234/parse',
|
||||
uri: 'mongodb://user!with%40%2B%20symbols:password!with%40%2B%20symbols@localhost:1234/parse'
|
||||
}).connect();
|
||||
expect(MongoClient.connect).toHaveBeenCalledWith(
|
||||
'mongodb://user!with%40%2B%20symbols:password!with%40%2B%20symbols@localhost:1234/parse',
|
||||
@@ -42,12 +35,11 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// https://github.com/parse-community/parse-server/pull/148#issuecomment-180407057
|
||||
// https://github.com/ParsePlatform/parse-server/pull/148#issuecomment-180407057
|
||||
it('preserves replica sets', () => {
|
||||
spyOn(MongoClient, 'connect').and.returnValue(Promise.resolve(fakeClient));
|
||||
spyOn(MongoClient, 'connect').and.returnValue(Promise.resolve(null));
|
||||
new MongoStorageAdapter({
|
||||
uri:
|
||||
'mongodb://test:testpass@ds056315-a0.mongolab.com:59325,ds059315-a1.mongolab.com:59315/testDBname?replicaSet=rs-ds059415',
|
||||
uri: 'mongodb://test:testpass@ds056315-a0.mongolab.com:59325,ds059315-a1.mongolab.com:59315/testDBname?replicaSet=rs-ds059415'
|
||||
}).connect();
|
||||
expect(MongoClient.connect).toHaveBeenCalledWith(
|
||||
'mongodb://test:testpass@ds056315-a0.mongolab.com:59325,ds059315-a1.mongolab.com:59315/testDBname?replicaSet=rs-ds059415',
|
||||
@@ -57,497 +49,192 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
|
||||
it('stores objectId in _id', done => {
|
||||
const adapter = new MongoStorageAdapter({ uri: databaseURI });
|
||||
adapter
|
||||
.createObject('Foo', { fields: {} }, { objectId: 'abcde' })
|
||||
.then(() => adapter._rawFind('Foo', {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
const obj = results[0];
|
||||
expect(obj._id).toEqual('abcde');
|
||||
expect(obj.objectId).toBeUndefined();
|
||||
adapter.createObject('Foo', { fields: {} }, { objectId: 'abcde' })
|
||||
.then(() => adapter._rawFind('Foo', {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
var obj = results[0];
|
||||
expect(obj._id).toEqual('abcde');
|
||||
expect(obj.objectId).toBeUndefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('find succeeds when query is within maxTimeMS', (done) => {
|
||||
const maxTimeMS = 250;
|
||||
const adapter = new MongoStorageAdapter({
|
||||
uri: databaseURI,
|
||||
mongoOptions: { maxTimeMS },
|
||||
});
|
||||
adapter.createObject('Foo', { fields: {} }, { objectId: 'abcde' })
|
||||
.then(() => adapter._rawFind('Foo', { '$where': `sleep(${maxTimeMS / 2})` }))
|
||||
.then(
|
||||
() => done(),
|
||||
(err) => {
|
||||
done.fail(`maxTimeMS should not affect fast queries ${err}`);
|
||||
}
|
||||
);
|
||||
})
|
||||
|
||||
it('find fails when query exceeds maxTimeMS', (done) => {
|
||||
const maxTimeMS = 250;
|
||||
const adapter = new MongoStorageAdapter({
|
||||
uri: databaseURI,
|
||||
mongoOptions: { maxTimeMS },
|
||||
});
|
||||
adapter.createObject('Foo', { fields: {} }, { objectId: 'abcde' })
|
||||
.then(() => adapter._rawFind('Foo', { '$where': `sleep(${maxTimeMS * 2})` }))
|
||||
.then(
|
||||
() => {
|
||||
done.fail('Find succeeded despite taking too long!');
|
||||
},
|
||||
(err) => {
|
||||
expect(err.name).toEqual('MongoError');
|
||||
expect(err.code).toEqual(50);
|
||||
expect(err.message).toEqual('operation exceeded time limit');
|
||||
done();
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('find succeeds when query is within maxTimeMS', done => {
|
||||
const maxTimeMS = 250;
|
||||
const adapter = new MongoStorageAdapter({
|
||||
uri: databaseURI,
|
||||
mongoOptions: { maxTimeMS },
|
||||
});
|
||||
adapter
|
||||
.createObject('Foo', { fields: {} }, { objectId: 'abcde' })
|
||||
.then(() => adapter._rawFind('Foo', { $where: `sleep(${maxTimeMS / 2})` }))
|
||||
.then(
|
||||
() => done(),
|
||||
err => {
|
||||
done.fail(`maxTimeMS should not affect fast queries ${err}`);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('find fails when query exceeds maxTimeMS', done => {
|
||||
const maxTimeMS = 250;
|
||||
const adapter = new MongoStorageAdapter({
|
||||
uri: databaseURI,
|
||||
mongoOptions: { maxTimeMS },
|
||||
});
|
||||
adapter
|
||||
.createObject('Foo', { fields: {} }, { objectId: 'abcde' })
|
||||
.then(() => adapter._rawFind('Foo', { $where: `sleep(${maxTimeMS * 2})` }))
|
||||
.then(
|
||||
() => {
|
||||
done.fail('Find succeeded despite taking too long!');
|
||||
},
|
||||
err => {
|
||||
expect(err.name).toEqual('MongoError');
|
||||
expect(err.code).toEqual(50);
|
||||
expect(err.message).toMatch('operation exceeded time limit');
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('stores pointers with a _p_ prefix', done => {
|
||||
it('stores pointers with a _p_ prefix', (done) => {
|
||||
const obj = {
|
||||
objectId: 'bar',
|
||||
aPointer: {
|
||||
__type: 'Pointer',
|
||||
className: 'JustThePointer',
|
||||
objectId: 'qwerty',
|
||||
},
|
||||
objectId: 'qwerty'
|
||||
}
|
||||
};
|
||||
const adapter = new MongoStorageAdapter({ uri: databaseURI });
|
||||
adapter
|
||||
.createObject(
|
||||
'APointerDarkly',
|
||||
{
|
||||
fields: {
|
||||
objectId: { type: 'String' },
|
||||
aPointer: { type: 'Pointer', targetClass: 'JustThePointer' },
|
||||
},
|
||||
},
|
||||
obj
|
||||
)
|
||||
.then(() => adapter._rawFind('APointerDarkly', {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
const output = results[0];
|
||||
expect(typeof output._id).toEqual('string');
|
||||
expect(typeof output._p_aPointer).toEqual('string');
|
||||
expect(output._p_aPointer).toEqual('JustThePointer$qwerty');
|
||||
expect(output.aPointer).toBeUndefined();
|
||||
done();
|
||||
});
|
||||
adapter.createObject('APointerDarkly', { fields: {
|
||||
objectId: { type: 'String' },
|
||||
aPointer: { type: 'Pointer', targetClass: 'JustThePointer' },
|
||||
}}, obj)
|
||||
.then(() => adapter._rawFind('APointerDarkly', {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
const output = results[0];
|
||||
expect(typeof output._id).toEqual('string');
|
||||
expect(typeof output._p_aPointer).toEqual('string');
|
||||
expect(output._p_aPointer).toEqual('JustThePointer$qwerty');
|
||||
expect(output.aPointer).toBeUndefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('handles object and subdocument', done => {
|
||||
const adapter = new MongoStorageAdapter({ uri: databaseURI });
|
||||
const schema = { fields: { subdoc: { type: 'Object' } } };
|
||||
const obj = { subdoc: { foo: 'bar', wu: 'tan' } };
|
||||
adapter
|
||||
.createObject('MyClass', schema, obj)
|
||||
.then(() => adapter._rawFind('MyClass', {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
const mob = results[0];
|
||||
expect(typeof mob.subdoc).toBe('object');
|
||||
expect(mob.subdoc.foo).toBe('bar');
|
||||
expect(mob.subdoc.wu).toBe('tan');
|
||||
const obj = { 'subdoc.wu': 'clan' };
|
||||
return adapter.findOneAndUpdate('MyClass', schema, {}, obj);
|
||||
})
|
||||
.then(() => adapter._rawFind('MyClass', {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
const mob = results[0];
|
||||
expect(typeof mob.subdoc).toBe('object');
|
||||
expect(mob.subdoc.foo).toBe('bar');
|
||||
expect(mob.subdoc.wu).toBe('clan');
|
||||
done();
|
||||
});
|
||||
const schema = { fields : { subdoc: { type: 'Object' } } };
|
||||
const obj = { subdoc: {foo: 'bar', wu: 'tan'} };
|
||||
adapter.createObject('MyClass', schema, obj)
|
||||
.then(() => adapter._rawFind('MyClass', {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
const mob = results[0];
|
||||
expect(typeof mob.subdoc).toBe('object');
|
||||
expect(mob.subdoc.foo).toBe('bar');
|
||||
expect(mob.subdoc.wu).toBe('tan');
|
||||
const obj = { 'subdoc.wu': 'clan' };
|
||||
return adapter.findOneAndUpdate('MyClass', schema, {}, obj);
|
||||
})
|
||||
.then(() => adapter._rawFind('MyClass', {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
const mob = results[0];
|
||||
expect(typeof mob.subdoc).toBe('object');
|
||||
expect(mob.subdoc.foo).toBe('bar');
|
||||
expect(mob.subdoc.wu).toBe('clan');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('handles creating an array, object, date', done => {
|
||||
it('handles creating an array, object, date', (done) => {
|
||||
const adapter = new MongoStorageAdapter({ uri: databaseURI });
|
||||
const obj = {
|
||||
array: [1, 2, 3],
|
||||
object: { foo: 'bar' },
|
||||
object: {foo: 'bar'},
|
||||
date: {
|
||||
__type: 'Date',
|
||||
iso: '2016-05-26T20:55:01.154Z',
|
||||
},
|
||||
};
|
||||
const schema = {
|
||||
fields: {
|
||||
array: { type: 'Array' },
|
||||
object: { type: 'Object' },
|
||||
date: { type: 'Date' },
|
||||
},
|
||||
};
|
||||
adapter
|
||||
.createObject('MyClass', schema, obj)
|
||||
.then(() => adapter._rawFind('MyClass', {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
const mob = results[0];
|
||||
expect(mob.array instanceof Array).toBe(true);
|
||||
expect(typeof mob.object).toBe('object');
|
||||
expect(mob.date instanceof Date).toBe(true);
|
||||
return adapter.find('MyClass', schema, {}, {});
|
||||
})
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
const mob = results[0];
|
||||
expect(mob.array instanceof Array).toBe(true);
|
||||
expect(typeof mob.object).toBe('object');
|
||||
expect(mob.date.__type).toBe('Date');
|
||||
expect(mob.date.iso).toBe('2016-05-26T20:55:01.154Z');
|
||||
done();
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
fail();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('handles updating a single object with array, object date', done => {
|
||||
const adapter = new MongoStorageAdapter({ uri: databaseURI });
|
||||
|
||||
const schema = {
|
||||
fields: {
|
||||
array: { type: 'Array' },
|
||||
object: { type: 'Object' },
|
||||
date: { type: 'Date' },
|
||||
},
|
||||
};
|
||||
|
||||
adapter
|
||||
.createObject('MyClass', schema, {})
|
||||
.then(() => adapter._rawFind('MyClass', {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
const update = {
|
||||
array: [1, 2, 3],
|
||||
object: { foo: 'bar' },
|
||||
date: {
|
||||
__type: 'Date',
|
||||
iso: '2016-05-26T20:55:01.154Z',
|
||||
},
|
||||
};
|
||||
const query = {};
|
||||
return adapter.findOneAndUpdate('MyClass', schema, query, update);
|
||||
})
|
||||
.then(results => {
|
||||
const mob = results;
|
||||
expect(mob.array instanceof Array).toBe(true);
|
||||
expect(typeof mob.object).toBe('object');
|
||||
expect(mob.date.__type).toBe('Date');
|
||||
expect(mob.date.iso).toBe('2016-05-26T20:55:01.154Z');
|
||||
return adapter._rawFind('MyClass', {});
|
||||
})
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
const mob = results[0];
|
||||
expect(mob.array instanceof Array).toBe(true);
|
||||
expect(typeof mob.object).toBe('object');
|
||||
expect(mob.date instanceof Date).toBe(true);
|
||||
done();
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
fail();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('handleShutdown, close connection', async () => {
|
||||
const adapter = new MongoStorageAdapter({ uri: databaseURI });
|
||||
|
||||
const schema = {
|
||||
fields: {
|
||||
array: { type: 'Array' },
|
||||
object: { type: 'Object' },
|
||||
date: { type: 'Date' },
|
||||
},
|
||||
};
|
||||
|
||||
await adapter.createObject('MyClass', schema, {});
|
||||
const status = await adapter.database.admin().serverStatus();
|
||||
expect(status.connections.current > 0).toEqual(true);
|
||||
|
||||
await adapter.handleShutdown();
|
||||
try {
|
||||
await adapter.database.admin().serverStatus();
|
||||
expect(false).toBe(true);
|
||||
} catch (e) {
|
||||
expect(e.message).toEqual('topology was destroyed');
|
||||
}
|
||||
});
|
||||
|
||||
it('getClass if exists', async () => {
|
||||
const adapter = new MongoStorageAdapter({ uri: databaseURI });
|
||||
|
||||
const schema = {
|
||||
fields: {
|
||||
array: { type: 'Array' },
|
||||
object: { type: 'Object' },
|
||||
date: { type: 'Date' },
|
||||
},
|
||||
};
|
||||
|
||||
await adapter.createClass('MyClass', schema);
|
||||
const myClassSchema = await adapter.getClass('MyClass');
|
||||
expect(myClassSchema).toBeDefined();
|
||||
});
|
||||
|
||||
it('getClass if not exists', async () => {
|
||||
const adapter = new MongoStorageAdapter({ uri: databaseURI });
|
||||
await expectAsync(adapter.getClass('UnknownClass')).toBeRejectedWith(undefined);
|
||||
});
|
||||
|
||||
it('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;
|
||||
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.executionStats.executionStages.stage).toBe('COLLSCAN');
|
||||
expect(postIndexPlan.executionStats.executionStages.stage).toBe('FETCH');
|
||||
});
|
||||
|
||||
it('should delete field without index', async () => {
|
||||
const database = Config.get(Parse.applicationId).database;
|
||||
const obj = new Parse.Object('MyObject');
|
||||
obj.set('test', 1);
|
||||
await obj.save();
|
||||
const schemaBeforeDeletion = await new Parse.Schema('MyObject').get();
|
||||
await database.adapter.deleteFields('MyObject', schemaBeforeDeletion, ['test']);
|
||||
const schemaAfterDeletion = await new Parse.Schema('MyObject').get();
|
||||
expect(schemaBeforeDeletion.fields.test).toBeDefined();
|
||||
expect(schemaAfterDeletion.fields.test).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should delete field with index', async () => {
|
||||
const database = Config.get(Parse.applicationId).database;
|
||||
const obj = new Parse.Object('MyObject');
|
||||
obj.set('test', 1);
|
||||
await obj.save();
|
||||
const schemaBeforeDeletion = await new Parse.Schema('MyObject').get();
|
||||
await database.adapter.ensureIndex('MyObject', schemaBeforeDeletion, ['test']);
|
||||
await database.adapter.deleteFields('MyObject', schemaBeforeDeletion, ['test']);
|
||||
const schemaAfterDeletion = await new Parse.Schema('MyObject').get();
|
||||
expect(schemaBeforeDeletion.fields.test).toBeDefined();
|
||||
expect(schemaAfterDeletion.fields.test).toBeUndefined();
|
||||
});
|
||||
|
||||
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',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI:
|
||||
'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase?replicaSet=replicaset',
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestUtils.destroyAllDataPermanently(true);
|
||||
});
|
||||
|
||||
it('should use transaction in a batch with transaction = true', async () => {
|
||||
const myObject = new Parse.Object('MyObject');
|
||||
await myObject.save();
|
||||
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
spyOn(databaseAdapter.database.serverConfig, 'command').and.callThrough();
|
||||
|
||||
await request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/batch',
|
||||
body: JSON.stringify({
|
||||
requests: [
|
||||
{
|
||||
method: 'PUT',
|
||||
path: '/1/classes/MyObject/' + myObject.id,
|
||||
body: { myAttribute: 'myValue' },
|
||||
},
|
||||
],
|
||||
transaction: true,
|
||||
}),
|
||||
});
|
||||
|
||||
let found = false;
|
||||
databaseAdapter.database.serverConfig.command.calls.all().forEach(call => {
|
||||
found = true;
|
||||
expect(call.args[2].session.transaction.state).not.toBe('NO_TRANSACTION');
|
||||
});
|
||||
expect(found).toBe(true);
|
||||
});
|
||||
|
||||
it('should not use transaction in a batch with transaction = false', async () => {
|
||||
const myObject = new Parse.Object('MyObject');
|
||||
await myObject.save();
|
||||
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
spyOn(databaseAdapter.database.serverConfig, 'command').and.callThrough();
|
||||
|
||||
await request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/batch',
|
||||
body: JSON.stringify({
|
||||
requests: [
|
||||
{
|
||||
method: 'PUT',
|
||||
path: '/1/classes/MyObject/' + myObject.id,
|
||||
body: { myAttribute: 'myValue' },
|
||||
},
|
||||
],
|
||||
transaction: false,
|
||||
}),
|
||||
});
|
||||
|
||||
let found = false;
|
||||
databaseAdapter.database.serverConfig.command.calls.all().forEach(call => {
|
||||
found = true;
|
||||
expect(call.args[2].session).toBe(undefined);
|
||||
});
|
||||
expect(found).toBe(true);
|
||||
});
|
||||
|
||||
it('should not use transaction in a batch with no transaction option sent', async () => {
|
||||
const myObject = new Parse.Object('MyObject');
|
||||
await myObject.save();
|
||||
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
spyOn(databaseAdapter.database.serverConfig, 'command').and.callThrough();
|
||||
|
||||
await request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/batch',
|
||||
body: JSON.stringify({
|
||||
requests: [
|
||||
{
|
||||
method: 'PUT',
|
||||
path: '/1/classes/MyObject/' + myObject.id,
|
||||
body: { myAttribute: 'myValue' },
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
let found = false;
|
||||
databaseAdapter.database.serverConfig.command.calls.all().forEach(call => {
|
||||
found = true;
|
||||
expect(call.args[2].session).toBe(undefined);
|
||||
});
|
||||
expect(found).toBe(true);
|
||||
});
|
||||
|
||||
it('should not use transaction in a put request', async () => {
|
||||
const myObject = new Parse.Object('MyObject');
|
||||
await myObject.save();
|
||||
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
spyOn(databaseAdapter.database.serverConfig, 'command').and.callThrough();
|
||||
|
||||
await request({
|
||||
method: 'PUT',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/classes/MyObject/' + myObject.id,
|
||||
body: { myAttribute: 'myValue' },
|
||||
});
|
||||
|
||||
let found = false;
|
||||
databaseAdapter.database.serverConfig.command.calls.all().forEach(call => {
|
||||
found = true;
|
||||
expect(call.args[2].session).toBe(undefined);
|
||||
});
|
||||
expect(found).toBe(true);
|
||||
});
|
||||
|
||||
it('should not use transactions when using SDK insert', async () => {
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
spyOn(databaseAdapter.database.serverConfig, 'insert').and.callThrough();
|
||||
|
||||
const myObject = new Parse.Object('MyObject');
|
||||
await myObject.save();
|
||||
|
||||
const calls = databaseAdapter.database.serverConfig.insert.calls.all();
|
||||
expect(calls.length).toBeGreaterThan(0);
|
||||
calls.forEach(call => {
|
||||
expect(call.args[2].session.transaction.state).toBe('NO_TRANSACTION');
|
||||
});
|
||||
});
|
||||
|
||||
it('should not use transactions when using SDK update', async () => {
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
spyOn(databaseAdapter.database.serverConfig, 'update').and.callThrough();
|
||||
|
||||
const myObject = new Parse.Object('MyObject');
|
||||
await myObject.save();
|
||||
|
||||
myObject.set('myAttribute', 'myValue');
|
||||
await myObject.save();
|
||||
|
||||
const calls = databaseAdapter.database.serverConfig.update.calls.all();
|
||||
expect(calls.length).toBeGreaterThan(0);
|
||||
calls.forEach(call => {
|
||||
expect(call.args[2].session.transaction.state).toBe('NO_TRANSACTION');
|
||||
});
|
||||
});
|
||||
|
||||
it('should not use transactions when using SDK delete', async () => {
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
spyOn(databaseAdapter.database.serverConfig, 'remove').and.callThrough();
|
||||
|
||||
const myObject = new Parse.Object('MyObject');
|
||||
await myObject.save();
|
||||
|
||||
await myObject.destroy();
|
||||
|
||||
const calls = databaseAdapter.database.serverConfig.remove.calls.all();
|
||||
expect(calls.length).toBeGreaterThan(0);
|
||||
calls.forEach(call => {
|
||||
expect(call.args[2].session.transaction.state).toBe('NO_TRANSACTION');
|
||||
});
|
||||
});
|
||||
const schema = { fields: {
|
||||
array: { type: 'Array' },
|
||||
object: { type: 'Object' },
|
||||
date: { type: 'Date' },
|
||||
} };
|
||||
adapter.createObject('MyClass', schema, obj)
|
||||
.then(() => adapter._rawFind('MyClass', {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
const mob = results[0];
|
||||
expect(mob.array instanceof Array).toBe(true);
|
||||
expect(typeof mob.object).toBe('object');
|
||||
expect(mob.date instanceof Date).toBe(true);
|
||||
return adapter.find('MyClass', schema, {}, {});
|
||||
})
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
const mob = results[0];
|
||||
expect(mob.array instanceof Array).toBe(true);
|
||||
expect(typeof mob.object).toBe('object');
|
||||
expect(mob.date.__type).toBe('Date');
|
||||
expect(mob.date.iso).toBe('2016-05-26T20:55:01.154Z');
|
||||
done();
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
fail();
|
||||
done();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("handles updating a single object with array, object date", (done) => {
|
||||
const adapter = new MongoStorageAdapter({ uri: databaseURI });
|
||||
|
||||
const schema = { fields: {
|
||||
array: { type: 'Array' },
|
||||
object: { type: 'Object' },
|
||||
date: { type: 'Date' },
|
||||
} };
|
||||
|
||||
|
||||
adapter.createObject('MyClass', schema, {})
|
||||
.then(() => adapter._rawFind('MyClass', {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
const update = {
|
||||
array: [1, 2, 3],
|
||||
object: {foo: 'bar'},
|
||||
date: {
|
||||
__type: 'Date',
|
||||
iso: '2016-05-26T20:55:01.154Z',
|
||||
},
|
||||
};
|
||||
const query = {};
|
||||
return adapter.findOneAndUpdate('MyClass', schema, query, update)
|
||||
})
|
||||
.then(results => {
|
||||
const mob = results;
|
||||
expect(mob.array instanceof Array).toBe(true);
|
||||
expect(typeof mob.object).toBe('object');
|
||||
expect(mob.date.__type).toBe('Date');
|
||||
expect(mob.date.iso).toBe('2016-05-26T20:55:01.154Z');
|
||||
return adapter._rawFind('MyClass', {});
|
||||
})
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
const mob = results[0];
|
||||
expect(mob.array instanceof Array).toBe(true);
|
||||
expect(typeof mob.object).toBe('object');
|
||||
expect(mob.date instanceof Date).toBe(true);
|
||||
done();
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
fail();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+129
-495
@@ -1,339 +1,233 @@
|
||||
// These tests are unit tests designed to only test transform.js.
|
||||
'use strict';
|
||||
"use strict";
|
||||
|
||||
const transform = require('../lib/Adapters/Storage/Mongo/MongoTransform');
|
||||
const transform = require('../src/Adapters/Storage/Mongo/MongoTransform');
|
||||
const dd = require('deep-diff');
|
||||
const mongodb = require('mongodb');
|
||||
|
||||
describe('parseObjectToMongoObjectForCreate', () => {
|
||||
it('a basic number', done => {
|
||||
const input = { five: 5 };
|
||||
const output = transform.parseObjectToMongoObjectForCreate(null, input, {
|
||||
fields: { five: { type: 'Number' } },
|
||||
it('a basic number', (done) => {
|
||||
var input = {five: 5};
|
||||
var output = transform.parseObjectToMongoObjectForCreate(null, input, {
|
||||
fields: {five: {type: 'Number'}}
|
||||
});
|
||||
jequal(input, output);
|
||||
done();
|
||||
});
|
||||
|
||||
it('an object with null values', done => {
|
||||
const input = { objectWithNullValues: { isNull: null, notNull: 3 } };
|
||||
const output = transform.parseObjectToMongoObjectForCreate(null, input, {
|
||||
fields: { objectWithNullValues: { type: 'object' } },
|
||||
it('an object with null values', (done) => {
|
||||
var input = {objectWithNullValues: {isNull: null, notNull: 3}};
|
||||
var output = transform.parseObjectToMongoObjectForCreate(null, input, {
|
||||
fields: {objectWithNullValues: {type: 'object'}}
|
||||
});
|
||||
jequal(input, output);
|
||||
done();
|
||||
});
|
||||
|
||||
it('built-in timestamps with date', done => {
|
||||
const input = {
|
||||
createdAt: '2015-10-06T21:24:50.332Z',
|
||||
updatedAt: '2015-10-06T21:24:50.332Z',
|
||||
it('built-in timestamps', (done) => {
|
||||
var input = {
|
||||
createdAt: "2015-10-06T21:24:50.332Z",
|
||||
updatedAt: "2015-10-06T21:24:50.332Z"
|
||||
};
|
||||
const output = transform.parseObjectToMongoObjectForCreate(null, input, {
|
||||
fields: {},
|
||||
});
|
||||
var output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} });
|
||||
expect(output._created_at instanceof Date).toBe(true);
|
||||
expect(output._updated_at instanceof Date).toBe(true);
|
||||
done();
|
||||
});
|
||||
|
||||
it('array of pointers', done => {
|
||||
const pointer = {
|
||||
it('array of pointers', (done) => {
|
||||
var pointer = {
|
||||
__type: 'Pointer',
|
||||
objectId: 'myId',
|
||||
className: 'Blah',
|
||||
};
|
||||
const out = transform.parseObjectToMongoObjectForCreate(
|
||||
null,
|
||||
{ pointers: [pointer] },
|
||||
{
|
||||
fields: { pointers: { type: 'Array' } },
|
||||
}
|
||||
);
|
||||
var out = transform.parseObjectToMongoObjectForCreate(null, {pointers: [pointer]},{
|
||||
fields: {pointers: {type: 'Array'}}
|
||||
});
|
||||
jequal([pointer], out.pointers);
|
||||
done();
|
||||
});
|
||||
|
||||
//TODO: object creation requests shouldn't be seeing __op delete, it makes no sense to
|
||||
//have __op delete in a new object. Figure out what this should actually be testing.
|
||||
xit('a delete op', done => {
|
||||
const input = { deleteMe: { __op: 'Delete' } };
|
||||
const output = transform.parseObjectToMongoObjectForCreate(null, input, {
|
||||
fields: {},
|
||||
});
|
||||
xit('a delete op', (done) => {
|
||||
var input = {deleteMe: {__op: 'Delete'}};
|
||||
var output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} });
|
||||
jequal(output, {});
|
||||
done();
|
||||
});
|
||||
|
||||
it('Doesnt allow ACL, as Parse Server should tranform ACL to _wperm + _rperm', done => {
|
||||
const input = { ACL: { '0123': { read: true, write: true } } };
|
||||
expect(() =>
|
||||
transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} })
|
||||
).toThrow();
|
||||
var input = {ACL: {'0123': {'read': true, 'write': true}}};
|
||||
expect(() => transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} })).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('parse geopoint to mongo', done => {
|
||||
const lat = -45;
|
||||
const lng = 45;
|
||||
const geoPoint = { __type: 'GeoPoint', latitude: lat, longitude: lng };
|
||||
const out = transform.parseObjectToMongoObjectForCreate(
|
||||
null,
|
||||
{ location: geoPoint },
|
||||
{
|
||||
fields: { location: { type: 'GeoPoint' } },
|
||||
}
|
||||
);
|
||||
expect(out.location).toEqual([lng, lat]);
|
||||
it('plain', (done) => {
|
||||
var geoPoint = {__type: 'GeoPoint', longitude: 180, latitude: -180};
|
||||
var out = transform.parseObjectToMongoObjectForCreate(null, {location: geoPoint},{
|
||||
fields: {location: {type: 'GeoPoint'}}
|
||||
});
|
||||
expect(out.location).toEqual([180, -180]);
|
||||
done();
|
||||
});
|
||||
|
||||
it('parse polygon to mongo', done => {
|
||||
const lat1 = -45;
|
||||
const lng1 = 45;
|
||||
const lat2 = -55;
|
||||
const lng2 = 55;
|
||||
const lat3 = -65;
|
||||
const lng3 = 65;
|
||||
const polygon = {
|
||||
__type: 'Polygon',
|
||||
coordinates: [
|
||||
[lat1, lng1],
|
||||
[lat2, lng2],
|
||||
[lat3, lng3],
|
||||
],
|
||||
};
|
||||
const out = transform.parseObjectToMongoObjectForCreate(
|
||||
null,
|
||||
{ location: polygon },
|
||||
{
|
||||
fields: { location: { type: 'Polygon' } },
|
||||
}
|
||||
);
|
||||
expect(out.location.coordinates).toEqual([
|
||||
[
|
||||
[lng1, lat1],
|
||||
[lng2, lat2],
|
||||
[lng3, lat3],
|
||||
[lng1, lat1],
|
||||
],
|
||||
]);
|
||||
done();
|
||||
});
|
||||
|
||||
it('in array', done => {
|
||||
const geoPoint = { __type: 'GeoPoint', longitude: 180, latitude: -180 };
|
||||
const out = transform.parseObjectToMongoObjectForCreate(
|
||||
null,
|
||||
{ locations: [geoPoint, geoPoint] },
|
||||
{
|
||||
fields: { locations: { type: 'Array' } },
|
||||
}
|
||||
);
|
||||
it('in array', (done) => {
|
||||
var geoPoint = {__type: 'GeoPoint', longitude: 180, latitude: -180};
|
||||
var out = transform.parseObjectToMongoObjectForCreate(null, {locations: [geoPoint, geoPoint]},{
|
||||
fields: {locations: {type: 'Array'}}
|
||||
});
|
||||
expect(out.locations).toEqual([geoPoint, geoPoint]);
|
||||
done();
|
||||
});
|
||||
|
||||
it('in sub-object', done => {
|
||||
const geoPoint = { __type: 'GeoPoint', longitude: 180, latitude: -180 };
|
||||
const out = transform.parseObjectToMongoObjectForCreate(
|
||||
null,
|
||||
{ locations: { start: geoPoint } },
|
||||
{
|
||||
fields: { locations: { type: 'Object' } },
|
||||
}
|
||||
);
|
||||
it('in sub-object', (done) => {
|
||||
var geoPoint = {__type: 'GeoPoint', longitude: 180, latitude: -180};
|
||||
var out = transform.parseObjectToMongoObjectForCreate(null, { locations: { start: geoPoint }},{
|
||||
fields: {locations: {type: 'Object'}}
|
||||
});
|
||||
expect(out).toEqual({ locations: { start: geoPoint } });
|
||||
done();
|
||||
});
|
||||
|
||||
it('objectId', done => {
|
||||
const out = transform.transformWhere(null, { objectId: 'foo' });
|
||||
it('objectId', (done) => {
|
||||
var out = transform.transformWhere(null, {objectId: 'foo'});
|
||||
expect(out._id).toEqual('foo');
|
||||
done();
|
||||
});
|
||||
|
||||
it('objectId in a list', done => {
|
||||
const input = {
|
||||
objectId: { $in: ['one', 'two', 'three'] },
|
||||
it('objectId in a list', (done) => {
|
||||
var input = {
|
||||
objectId: {'$in': ['one', 'two', 'three']},
|
||||
};
|
||||
const output = transform.transformWhere(null, input);
|
||||
var output = transform.transformWhere(null, input);
|
||||
jequal(input.objectId, output._id);
|
||||
done();
|
||||
});
|
||||
|
||||
it('built-in timestamps', done => {
|
||||
const input = { createdAt: new Date(), updatedAt: new Date() };
|
||||
const output = transform.mongoObjectToParseObject(null, input, {
|
||||
fields: {},
|
||||
});
|
||||
it('built-in timestamps', (done) => {
|
||||
var input = {createdAt: new Date(), updatedAt: new Date()};
|
||||
var output = transform.mongoObjectToParseObject(null, input, { fields: {} });
|
||||
expect(typeof output.createdAt).toEqual('string');
|
||||
expect(typeof output.updatedAt).toEqual('string');
|
||||
done();
|
||||
});
|
||||
|
||||
it('pointer', done => {
|
||||
const input = { _p_userPointer: '_User$123' };
|
||||
const output = transform.mongoObjectToParseObject(null, input, {
|
||||
it('pointer', (done) => {
|
||||
var input = {_p_userPointer: '_User$123'};
|
||||
var output = transform.mongoObjectToParseObject(null, input, {
|
||||
fields: { userPointer: { type: 'Pointer', targetClass: '_User' } },
|
||||
});
|
||||
expect(typeof output.userPointer).toEqual('object');
|
||||
expect(output.userPointer).toEqual({
|
||||
__type: 'Pointer',
|
||||
className: '_User',
|
||||
objectId: '123',
|
||||
});
|
||||
expect(output.userPointer).toEqual(
|
||||
{__type: 'Pointer', className: '_User', objectId: '123'}
|
||||
);
|
||||
done();
|
||||
});
|
||||
|
||||
it('null pointer', done => {
|
||||
const input = { _p_userPointer: null };
|
||||
const output = transform.mongoObjectToParseObject(null, input, {
|
||||
it('null pointer', (done) => {
|
||||
var input = {_p_userPointer: null};
|
||||
var output = transform.mongoObjectToParseObject(null, input, {
|
||||
fields: { userPointer: { type: 'Pointer', targetClass: '_User' } },
|
||||
});
|
||||
expect(output.userPointer).toBeUndefined();
|
||||
done();
|
||||
});
|
||||
|
||||
it('file', done => {
|
||||
const input = { picture: 'pic.jpg' };
|
||||
const output = transform.mongoObjectToParseObject(null, input, {
|
||||
fields: { picture: { type: 'File' } },
|
||||
it('file', (done) => {
|
||||
var input = {picture: 'pic.jpg'};
|
||||
var output = transform.mongoObjectToParseObject(null, input, {
|
||||
fields: { picture: { type: 'File' }},
|
||||
});
|
||||
expect(typeof output.picture).toEqual('object');
|
||||
expect(output.picture).toEqual({ __type: 'File', name: 'pic.jpg' });
|
||||
expect(output.picture).toEqual({__type: 'File', name: 'pic.jpg'});
|
||||
done();
|
||||
});
|
||||
|
||||
it('mongo geopoint to parse', done => {
|
||||
const lat = -45;
|
||||
const lng = 45;
|
||||
const input = { location: [lng, lat] };
|
||||
const output = transform.mongoObjectToParseObject(null, input, {
|
||||
fields: { location: { type: 'GeoPoint' } },
|
||||
it('geopoint', (done) => {
|
||||
var input = {location: [180, -180]};
|
||||
var output = transform.mongoObjectToParseObject(null, input, {
|
||||
fields: { location: { type: 'GeoPoint' }},
|
||||
});
|
||||
expect(typeof output.location).toEqual('object');
|
||||
expect(output.location).toEqual({
|
||||
__type: 'GeoPoint',
|
||||
latitude: lat,
|
||||
longitude: lng,
|
||||
});
|
||||
expect(output.location).toEqual(
|
||||
{__type: 'GeoPoint', longitude: 180, latitude: -180}
|
||||
);
|
||||
done();
|
||||
});
|
||||
|
||||
it('mongo polygon to parse', done => {
|
||||
const lat = -45;
|
||||
const lng = 45;
|
||||
// Mongo stores polygon in WGS84 lng/lat
|
||||
const input = {
|
||||
location: {
|
||||
type: 'Polygon',
|
||||
coordinates: [
|
||||
[
|
||||
[lat, lng],
|
||||
[lat, lng],
|
||||
],
|
||||
],
|
||||
},
|
||||
};
|
||||
const output = transform.mongoObjectToParseObject(null, input, {
|
||||
fields: { location: { type: 'Polygon' } },
|
||||
});
|
||||
expect(typeof output.location).toEqual('object');
|
||||
expect(output.location).toEqual({
|
||||
__type: 'Polygon',
|
||||
coordinates: [
|
||||
[lng, lat],
|
||||
[lng, lat],
|
||||
],
|
||||
});
|
||||
done();
|
||||
});
|
||||
|
||||
it('bytes', done => {
|
||||
const input = { binaryData: 'aGVsbG8gd29ybGQ=' };
|
||||
const output = transform.mongoObjectToParseObject(null, input, {
|
||||
fields: { binaryData: { type: 'Bytes' } },
|
||||
it('bytes', (done) => {
|
||||
var input = {binaryData: "aGVsbG8gd29ybGQ="};
|
||||
var output = transform.mongoObjectToParseObject(null, input, {
|
||||
fields: { binaryData: { type: 'Bytes' }},
|
||||
});
|
||||
expect(typeof output.binaryData).toEqual('object');
|
||||
expect(output.binaryData).toEqual({
|
||||
__type: 'Bytes',
|
||||
base64: 'aGVsbG8gd29ybGQ=',
|
||||
});
|
||||
expect(output.binaryData).toEqual(
|
||||
{__type: 'Bytes', base64: "aGVsbG8gd29ybGQ="}
|
||||
);
|
||||
done();
|
||||
});
|
||||
|
||||
it('nested array', done => {
|
||||
const input = { arr: [{ _testKey: 'testValue' }] };
|
||||
const output = transform.mongoObjectToParseObject(null, input, {
|
||||
it('nested array', (done) => {
|
||||
var input = {arr: [{_testKey: 'testValue' }]};
|
||||
var output = transform.mongoObjectToParseObject(null, input, {
|
||||
fields: { arr: { type: 'Array' } },
|
||||
});
|
||||
expect(Array.isArray(output.arr)).toEqual(true);
|
||||
expect(output.arr).toEqual([{ _testKey: 'testValue' }]);
|
||||
expect(output.arr).toEqual([{ _testKey: 'testValue'}]);
|
||||
done();
|
||||
});
|
||||
|
||||
it('untransforms objects containing nested special keys', done => {
|
||||
const input = {
|
||||
array: [
|
||||
{
|
||||
_id: 'Test ID',
|
||||
_hashed_password:
|
||||
"I Don't know why you would name a key this, but if you do it should work",
|
||||
_tombstone: {
|
||||
_updated_at: "I'm sure people will nest keys like this",
|
||||
_acl: 7,
|
||||
_id: { someString: 'str', someNumber: 7 },
|
||||
regularKey: { moreContents: [1, 2, 3] },
|
||||
},
|
||||
regularKey: 'some data',
|
||||
},
|
||||
],
|
||||
};
|
||||
const input = {array: [{
|
||||
_id: "Test ID",
|
||||
_hashed_password: "I Don't know why you would name a key this, but if you do it should work",
|
||||
_tombstone: {
|
||||
_updated_at: "I'm sure people will nest keys like this",
|
||||
_acl: 7,
|
||||
_id: { someString: "str", someNumber: 7},
|
||||
regularKey: { moreContents: [1, 2, 3] },
|
||||
},
|
||||
regularKey: "some data",
|
||||
}]}
|
||||
const output = transform.mongoObjectToParseObject(null, input, {
|
||||
fields: { array: { type: 'Array' } },
|
||||
fields: { array: { type: 'Array' }},
|
||||
});
|
||||
expect(dd(output, input)).toEqual(undefined);
|
||||
done();
|
||||
});
|
||||
|
||||
it('changes new pointer key', done => {
|
||||
const input = {
|
||||
somePointer: { __type: 'Pointer', className: 'Micro', objectId: 'oft' },
|
||||
it('changes new pointer key', (done) => {
|
||||
var input = {
|
||||
somePointer: {__type: 'Pointer', className: 'Micro', objectId: 'oft'}
|
||||
};
|
||||
const output = transform.parseObjectToMongoObjectForCreate(null, input, {
|
||||
fields: { somePointer: { type: 'Pointer' } },
|
||||
var output = transform.parseObjectToMongoObjectForCreate(null, input, {
|
||||
fields: {somePointer: {type: 'Pointer'}}
|
||||
});
|
||||
expect(typeof output._p_somePointer).toEqual('string');
|
||||
expect(output._p_somePointer).toEqual('Micro$oft');
|
||||
done();
|
||||
});
|
||||
|
||||
it('changes existing pointer keys', done => {
|
||||
const input = {
|
||||
userPointer: {
|
||||
__type: 'Pointer',
|
||||
className: '_User',
|
||||
objectId: 'qwerty',
|
||||
},
|
||||
it('changes existing pointer keys', (done) => {
|
||||
var input = {
|
||||
userPointer: {__type: 'Pointer', className: '_User', objectId: 'qwerty'}
|
||||
};
|
||||
const output = transform.parseObjectToMongoObjectForCreate(null, input, {
|
||||
fields: { userPointer: { type: 'Pointer' } },
|
||||
var output = transform.parseObjectToMongoObjectForCreate(null, input, {
|
||||
fields: {userPointer: {type: 'Pointer'}}
|
||||
});
|
||||
expect(typeof output._p_userPointer).toEqual('string');
|
||||
expect(output._p_userPointer).toEqual('_User$qwerty');
|
||||
done();
|
||||
});
|
||||
|
||||
it('writes the old ACL format in addition to rperm and wperm on create', done => {
|
||||
const input = {
|
||||
it('writes the old ACL format in addition to rperm and wperm on create', (done) => {
|
||||
var input = {
|
||||
_rperm: ['*'],
|
||||
_wperm: ['Kevin'],
|
||||
};
|
||||
|
||||
const output = transform.parseObjectToMongoObjectForCreate(null, input, {
|
||||
fields: {},
|
||||
});
|
||||
var output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} });
|
||||
expect(typeof output._acl).toEqual('object');
|
||||
expect(output._acl['Kevin'].w).toBeTruthy();
|
||||
expect(output._acl['Kevin'].r).toBeUndefined();
|
||||
@@ -342,11 +236,11 @@ describe('parseObjectToMongoObjectForCreate', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('removes Relation types', done => {
|
||||
const input = {
|
||||
it('removes Relation types', (done) => {
|
||||
var input = {
|
||||
aRelation: { __type: 'Relation', className: 'Stuff' },
|
||||
};
|
||||
const output = transform.parseObjectToMongoObjectForCreate(null, input, {
|
||||
var output = transform.parseObjectToMongoObjectForCreate(null, input, {
|
||||
fields: {
|
||||
aRelation: { __type: 'Relation', className: 'Stuff' },
|
||||
},
|
||||
@@ -355,14 +249,14 @@ describe('parseObjectToMongoObjectForCreate', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('writes the old ACL format in addition to rperm and wperm on update', done => {
|
||||
const input = {
|
||||
it('writes the old ACL format in addition to rperm and wperm on update', (done) => {
|
||||
var input = {
|
||||
_rperm: ['*'],
|
||||
_wperm: ['Kevin'],
|
||||
_wperm: ['Kevin']
|
||||
};
|
||||
|
||||
const output = transform.transformUpdate(null, input, { fields: {} });
|
||||
const set = output.$set;
|
||||
var output = transform.transformUpdate(null, input, { fields: {} });
|
||||
var set = output.$set;
|
||||
expect(typeof set).toEqual('object');
|
||||
expect(typeof set._acl).toEqual('object');
|
||||
expect(set._acl['Kevin'].w).toBeTruthy();
|
||||
@@ -372,26 +266,24 @@ describe('parseObjectToMongoObjectForCreate', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('untransforms from _rperm and _wperm to ACL', done => {
|
||||
const input = {
|
||||
_rperm: ['*'],
|
||||
_wperm: ['Kevin'],
|
||||
it('untransforms from _rperm and _wperm to ACL', (done) => {
|
||||
var input = {
|
||||
_rperm: ["*"],
|
||||
_wperm: ["Kevin"]
|
||||
};
|
||||
const output = transform.mongoObjectToParseObject(null, input, {
|
||||
fields: {},
|
||||
});
|
||||
var output = transform.mongoObjectToParseObject(null, input, { fields: {} });
|
||||
expect(output._rperm).toEqual(['*']);
|
||||
expect(output._wperm).toEqual(['Kevin']);
|
||||
expect(output.ACL).toBeUndefined();
|
||||
expect(output.ACL).toBeUndefined()
|
||||
done();
|
||||
});
|
||||
|
||||
it('untransforms mongodb number types', done => {
|
||||
const input = {
|
||||
it('untransforms mongodb number types', (done) => {
|
||||
var input = {
|
||||
long: mongodb.Long.fromNumber(Number.MAX_SAFE_INTEGER),
|
||||
double: new mongodb.Double(Number.MAX_VALUE),
|
||||
};
|
||||
const output = transform.mongoObjectToParseObject(null, input, {
|
||||
double: new mongodb.Double(Number.MAX_VALUE)
|
||||
}
|
||||
var output = transform.mongoObjectToParseObject(null, input, {
|
||||
fields: {
|
||||
long: { type: 'Number' },
|
||||
double: { type: 'Number' },
|
||||
@@ -401,148 +293,14 @@ describe('parseObjectToMongoObjectForCreate', () => {
|
||||
expect(output.double).toBe(Number.MAX_VALUE);
|
||||
done();
|
||||
});
|
||||
|
||||
it('Date object where iso attribute is of type Date', done => {
|
||||
const input = {
|
||||
ts: { __type: 'Date', iso: new Date('2017-01-18T00:00:00.000Z') },
|
||||
};
|
||||
const output = transform.mongoObjectToParseObject(null, input, {
|
||||
fields: {
|
||||
ts: { type: 'Date' },
|
||||
},
|
||||
});
|
||||
expect(output.ts.iso).toEqual('2017-01-18T00:00:00.000Z');
|
||||
done();
|
||||
});
|
||||
|
||||
it('Date object where iso attribute is of type String', done => {
|
||||
const input = {
|
||||
ts: { __type: 'Date', iso: '2017-01-18T00:00:00.000Z' },
|
||||
};
|
||||
const output = transform.mongoObjectToParseObject(null, input, {
|
||||
fields: {
|
||||
ts: { type: 'Date' },
|
||||
},
|
||||
});
|
||||
expect(output.ts.iso).toEqual('2017-01-18T00:00:00.000Z');
|
||||
done();
|
||||
});
|
||||
|
||||
it('object with undefined nested values', () => {
|
||||
const input = {
|
||||
_id: 'vQHyinCW1l',
|
||||
urls: { firstUrl: 'https://', secondUrl: undefined },
|
||||
};
|
||||
const output = transform.mongoObjectToParseObject(null, input, {
|
||||
fields: {
|
||||
urls: { type: 'Object' },
|
||||
},
|
||||
});
|
||||
expect(output.urls).toEqual({
|
||||
firstUrl: 'https://',
|
||||
secondUrl: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('undefined objects', () => {
|
||||
const input = {
|
||||
_id: 'vQHyinCW1l',
|
||||
urls: undefined,
|
||||
};
|
||||
const output = transform.mongoObjectToParseObject(null, input, {
|
||||
fields: {
|
||||
urls: { type: 'Object' },
|
||||
},
|
||||
});
|
||||
expect(output.urls).toBeUndefined();
|
||||
});
|
||||
|
||||
it('$regex in $all list', done => {
|
||||
const input = {
|
||||
arrayField: {
|
||||
$all: [{ $regex: '^\\Qone\\E' }, { $regex: '^\\Qtwo\\E' }, { $regex: '^\\Qthree\\E' }],
|
||||
},
|
||||
};
|
||||
const outputValue = {
|
||||
arrayField: { $all: [/^\Qone\E/, /^\Qtwo\E/, /^\Qthree\E/] },
|
||||
};
|
||||
|
||||
const output = transform.transformWhere(null, input);
|
||||
jequal(outputValue.arrayField, output.arrayField);
|
||||
done();
|
||||
});
|
||||
|
||||
it('$regex in $all list must be { $regex: "string" }', done => {
|
||||
const input = {
|
||||
arrayField: { $all: [{ $regex: 1 }] },
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
transform.transformWhere(null, input);
|
||||
}).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('all values in $all must be $regex (start with string) or non $regex (start with string)', done => {
|
||||
const input = {
|
||||
arrayField: {
|
||||
$all: [{ $regex: '^\\Qone\\E' }, { $unknown: '^\\Qtwo\\E' }],
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
transform.transformWhere(null, input);
|
||||
}).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('ignores User authData field in DB so it can be synthesized in code', done => {
|
||||
const input = {
|
||||
_id: '123',
|
||||
_auth_data_acme: { id: 'abc' },
|
||||
authData: null,
|
||||
};
|
||||
const output = transform.mongoObjectToParseObject('_User', input, {
|
||||
fields: {},
|
||||
});
|
||||
expect(output.authData.acme.id).toBe('abc');
|
||||
done();
|
||||
});
|
||||
|
||||
it('can set authData when not User class', done => {
|
||||
const input = {
|
||||
_id: '123',
|
||||
authData: 'random',
|
||||
};
|
||||
const output = transform.mongoObjectToParseObject('TestObject', input, {
|
||||
fields: {},
|
||||
});
|
||||
expect(output.authData).toBe('random');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot have a custom field name beginning with underscore', done => {
|
||||
const input = {
|
||||
_id: '123',
|
||||
_thisFieldNameIs: 'invalid',
|
||||
};
|
||||
try {
|
||||
transform.mongoObjectToParseObject('TestObject', input, {
|
||||
fields: {},
|
||||
});
|
||||
} catch (e) {
|
||||
expect(e).toBeDefined();
|
||||
}
|
||||
done();
|
||||
});
|
||||
|
||||
describe('transformUpdate', () => {
|
||||
it('removes Relation types', done => {
|
||||
const input = {
|
||||
it('removes Relation types', (done) => {
|
||||
var input = {
|
||||
aRelation: { __type: 'Relation', className: 'Stuff' },
|
||||
};
|
||||
const output = transform.transformUpdate(null, input, {
|
||||
var output = transform.transformUpdate(null, input, {
|
||||
fields: {
|
||||
aRelation: { __type: 'Relation', className: 'Stuff' },
|
||||
},
|
||||
@@ -551,127 +309,3 @@ describe('transformUpdate', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformConstraint', () => {
|
||||
describe('$relativeTime', () => {
|
||||
it('should error on $eq, $ne, and $exists', () => {
|
||||
expect(() => {
|
||||
transform.transformConstraint({
|
||||
$eq: {
|
||||
ttl: {
|
||||
$relativeTime: '12 days ago',
|
||||
},
|
||||
},
|
||||
});
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
transform.transformConstraint({
|
||||
$ne: {
|
||||
ttl: {
|
||||
$relativeTime: '12 days ago',
|
||||
},
|
||||
},
|
||||
});
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
transform.transformConstraint({
|
||||
$exists: {
|
||||
$relativeTime: '12 days ago',
|
||||
},
|
||||
});
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('relativeTimeToDate', () => {
|
||||
const now = new Date('2017-09-26T13:28:16.617Z');
|
||||
|
||||
describe('In the future', () => {
|
||||
it('should parse valid natural time', () => {
|
||||
const text = 'in 1 year 2 weeks 12 days 10 hours 24 minutes 30 seconds';
|
||||
const { result, status, info } = transform.relativeTimeToDate(text, now);
|
||||
expect(result.toISOString()).toBe('2018-10-22T23:52:46.617Z');
|
||||
expect(status).toBe('success');
|
||||
expect(info).toBe('future');
|
||||
});
|
||||
});
|
||||
|
||||
describe('In the past', () => {
|
||||
it('should parse valid natural time', () => {
|
||||
const text = '2 days 12 hours 1 minute 12 seconds ago';
|
||||
const { result, status, info } = transform.relativeTimeToDate(text, now);
|
||||
expect(result.toISOString()).toBe('2017-09-24T01:27:04.617Z');
|
||||
expect(status).toBe('success');
|
||||
expect(info).toBe('past');
|
||||
});
|
||||
});
|
||||
|
||||
describe('From now', () => {
|
||||
it('should equal current time', () => {
|
||||
const text = 'now';
|
||||
const { result, status, info } = transform.relativeTimeToDate(text, now);
|
||||
expect(result.toISOString()).toBe('2017-09-26T13:28:16.617Z');
|
||||
expect(status).toBe('success');
|
||||
expect(info).toBe('present');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error cases', () => {
|
||||
it('should error if string is completely gibberish', () => {
|
||||
expect(transform.relativeTimeToDate('gibberishasdnklasdnjklasndkl123j123')).toEqual({
|
||||
status: 'error',
|
||||
info: "Time should either start with 'in' or end with 'ago'",
|
||||
});
|
||||
});
|
||||
|
||||
it('should error if string contains neither `ago` nor `in`', () => {
|
||||
expect(transform.relativeTimeToDate('12 hours 1 minute')).toEqual({
|
||||
status: 'error',
|
||||
info: "Time should either start with 'in' or end with 'ago'",
|
||||
});
|
||||
});
|
||||
|
||||
it('should error if there are missing units or numbers', () => {
|
||||
expect(transform.relativeTimeToDate('in 12 hours 1')).toEqual({
|
||||
status: 'error',
|
||||
info: 'Invalid time string. Dangling unit or number.',
|
||||
});
|
||||
|
||||
expect(transform.relativeTimeToDate('12 hours minute ago')).toEqual({
|
||||
status: 'error',
|
||||
info: 'Invalid time string. Dangling unit or number.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should error on floating point numbers', () => {
|
||||
expect(transform.relativeTimeToDate('in 12.3 hours')).toEqual({
|
||||
status: 'error',
|
||||
info: "'12.3' is not an integer.",
|
||||
});
|
||||
});
|
||||
|
||||
it('should error if numbers are invalid', () => {
|
||||
expect(transform.relativeTimeToDate('12 hours 123a minute ago')).toEqual({
|
||||
status: 'error',
|
||||
info: "'123a' is not an integer.",
|
||||
});
|
||||
});
|
||||
|
||||
it('should error on invalid interval units', () => {
|
||||
expect(transform.relativeTimeToDate('4 score 7 years ago')).toEqual({
|
||||
status: 'error',
|
||||
info: "Invalid interval: 'score'",
|
||||
});
|
||||
});
|
||||
|
||||
it("should error when string contains 'ago' and 'in'", () => {
|
||||
expect(transform.relativeTimeToDate('in 1 day 2 minutes ago')).toEqual({
|
||||
status: 'error',
|
||||
info: "Time cannot have both 'in' and 'ago'",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,32 +1,37 @@
|
||||
const NullCacheAdapter = require('../lib/Adapters/Cache/NullCacheAdapter').default;
|
||||
var NullCacheAdapter = require('../src/Adapters/Cache/NullCacheAdapter').default;
|
||||
|
||||
describe('NullCacheAdapter', function () {
|
||||
const KEY = 'hello';
|
||||
const VALUE = 'world';
|
||||
describe('NullCacheAdapter', function() {
|
||||
var KEY = 'hello';
|
||||
var VALUE = 'world';
|
||||
|
||||
it('should expose promisifyed methods', done => {
|
||||
const cache = new NullCacheAdapter({
|
||||
ttl: NaN,
|
||||
it('should expose promisifyed methods', (done) => {
|
||||
var cache = new NullCacheAdapter({
|
||||
ttl: NaN
|
||||
});
|
||||
|
||||
// Verify all methods return promises.
|
||||
Promise.all([cache.put(KEY, VALUE), cache.del(KEY), cache.get(KEY), cache.clear()]).then(() => {
|
||||
Promise.all([
|
||||
cache.put(KEY, VALUE),
|
||||
cache.del(KEY),
|
||||
cache.get(KEY),
|
||||
cache.clear()
|
||||
]).then(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should get/set/clear', done => {
|
||||
const cache = new NullCacheAdapter({
|
||||
ttl: NaN,
|
||||
it('should get/set/clear', (done) => {
|
||||
var cache = new NullCacheAdapter({
|
||||
ttl: NaN
|
||||
});
|
||||
|
||||
cache
|
||||
.put(KEY, VALUE)
|
||||
cache.put(KEY, VALUE)
|
||||
.then(() => cache.get(KEY))
|
||||
.then(value => expect(value).toEqual(null))
|
||||
.then((value) => expect(value).toEqual(null))
|
||||
.then(() => cache.clear())
|
||||
.then(() => cache.get(KEY))
|
||||
.then(value => expect(value).toEqual(null))
|
||||
.then((value) => expect(value).toEqual(null))
|
||||
.then(done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
+80
-106
@@ -1,162 +1,136 @@
|
||||
const OAuth = require('../lib/Adapters/Auth/OAuth1Client');
|
||||
var OAuth = require("../src/Adapters/Auth/OAuth1Client");
|
||||
|
||||
describe('OAuth', function () {
|
||||
it('Nonce should have right length', done => {
|
||||
describe('OAuth', function() {
|
||||
it("Nonce should have right length", (done) => {
|
||||
jequal(OAuth.nonce().length, 30);
|
||||
done();
|
||||
});
|
||||
|
||||
it('Should properly build parameter string', done => {
|
||||
const string = OAuth.buildParameterString({ c: 1, a: 2, b: 3 });
|
||||
jequal(string, 'a=2&b=3&c=1');
|
||||
it("Should properly build parameter string", (done) => {
|
||||
var string = OAuth.buildParameterString({c:1, a:2, b:3})
|
||||
jequal(string, "a=2&b=3&c=1");
|
||||
done();
|
||||
});
|
||||
|
||||
it('Should properly build empty parameter string', done => {
|
||||
const string = OAuth.buildParameterString();
|
||||
jequal(string, '');
|
||||
it("Should properly build empty parameter string", (done) => {
|
||||
var string = OAuth.buildParameterString()
|
||||
jequal(string, "");
|
||||
done();
|
||||
});
|
||||
|
||||
it('Should properly build signature string', done => {
|
||||
const string = OAuth.buildSignatureString('get', 'http://dummy.com', '');
|
||||
jequal(string, 'GET&http%3A%2F%2Fdummy.com&');
|
||||
it("Should properly build signature string", (done) => {
|
||||
var string = OAuth.buildSignatureString("get", "http://dummy.com", "");
|
||||
jequal(string, "GET&http%3A%2F%2Fdummy.com&");
|
||||
done();
|
||||
});
|
||||
|
||||
it('Should properly generate request signature', done => {
|
||||
let request = {
|
||||
host: 'dummy.com',
|
||||
path: 'path',
|
||||
it("Should properly generate request signature", (done) => {
|
||||
var request = {
|
||||
host: "dummy.com",
|
||||
path: "path"
|
||||
};
|
||||
|
||||
const oauth_params = {
|
||||
var oauth_params = {
|
||||
oauth_timestamp: 123450000,
|
||||
oauth_nonce: 'AAAAAAAAAAAAAAAAA',
|
||||
oauth_consumer_key: 'hello',
|
||||
oauth_token: 'token',
|
||||
oauth_nonce: "AAAAAAAAAAAAAAAAA",
|
||||
oauth_consumer_key: "hello",
|
||||
oauth_token: "token"
|
||||
};
|
||||
|
||||
const consumer_secret = 'world';
|
||||
const auth_token_secret = 'secret';
|
||||
var consumer_secret = "world";
|
||||
var auth_token_secret = "secret";
|
||||
request = OAuth.signRequest(request, oauth_params, consumer_secret, auth_token_secret);
|
||||
jequal(
|
||||
request.headers['Authorization'],
|
||||
'OAuth oauth_consumer_key="hello", oauth_nonce="AAAAAAAAAAAAAAAAA", oauth_signature="8K95bpQcDi9Nd2GkhumTVcw4%2BXw%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="123450000", oauth_token="token", oauth_version="1.0"'
|
||||
);
|
||||
jequal(request.headers['Authorization'], 'OAuth oauth_consumer_key="hello", oauth_nonce="AAAAAAAAAAAAAAAAA", oauth_signature="8K95bpQcDi9Nd2GkhumTVcw4%2BXw%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="123450000", oauth_token="token", oauth_version="1.0"');
|
||||
done();
|
||||
});
|
||||
|
||||
it('Should properly build request', done => {
|
||||
const options = {
|
||||
host: 'dummy.com',
|
||||
consumer_key: 'hello',
|
||||
consumer_secret: 'world',
|
||||
auth_token: 'token',
|
||||
auth_token_secret: 'secret',
|
||||
it("Should properly build request", (done) => {
|
||||
var options = {
|
||||
host: "dummy.com",
|
||||
consumer_key: "hello",
|
||||
consumer_secret: "world",
|
||||
auth_token: "token",
|
||||
auth_token_secret: "secret",
|
||||
// Custom oauth params for tests
|
||||
oauth_params: {
|
||||
oauth_timestamp: 123450000,
|
||||
oauth_nonce: 'AAAAAAAAAAAAAAAAA',
|
||||
},
|
||||
oauth_nonce: "AAAAAAAAAAAAAAAAA"
|
||||
}
|
||||
};
|
||||
const path = 'path';
|
||||
const method = 'get';
|
||||
var path = "path";
|
||||
var method = "get";
|
||||
|
||||
const oauthClient = new OAuth(options);
|
||||
const req = oauthClient.buildRequest(method, path, { query: 'param' });
|
||||
var oauthClient = new OAuth(options);
|
||||
var req = oauthClient.buildRequest(method, path, {"query": "param"});
|
||||
|
||||
jequal(req.host, options.host);
|
||||
jequal(req.path, '/' + path + '?query=param');
|
||||
jequal(req.method, 'GET');
|
||||
jequal(req.path, "/"+path+"?query=param");
|
||||
jequal(req.method, "GET");
|
||||
jequal(req.headers['Content-Type'], 'application/x-www-form-urlencoded');
|
||||
jequal(
|
||||
req.headers['Authorization'],
|
||||
'OAuth oauth_consumer_key="hello", oauth_nonce="AAAAAAAAAAAAAAAAA", oauth_signature="wNkyEkDE%2F0JZ2idmqyrgHdvC0rs%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="123450000", oauth_token="token", oauth_version="1.0"'
|
||||
);
|
||||
jequal(req.headers['Authorization'], 'OAuth oauth_consumer_key="hello", oauth_nonce="AAAAAAAAAAAAAAAAA", oauth_signature="wNkyEkDE%2F0JZ2idmqyrgHdvC0rs%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="123450000", oauth_token="token", oauth_version="1.0"')
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
function validateCannotAuthenticateError(data, done) {
|
||||
jequal(typeof data, 'object');
|
||||
jequal(typeof data.errors, 'object');
|
||||
const errors = data.errors;
|
||||
jequal(typeof errors[0], 'object');
|
||||
jequal(typeof data, "object");
|
||||
jequal(typeof data.errors, "object");
|
||||
var errors = data.errors;
|
||||
jequal(typeof errors[0], "object");
|
||||
// Cannot authenticate error
|
||||
jequal(errors[0].code, 32);
|
||||
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.
|
||||
*/
|
||||
const options = {
|
||||
host: 'api.twitter.com',
|
||||
consumer_key: 'invalid_consumer_key',
|
||||
consumer_secret: 'invalid_consumer_secret',
|
||||
it("Should fail a GET request", (done) => {
|
||||
var options = {
|
||||
host: "api.twitter.com",
|
||||
consumer_key: "XXXXXXXXXXXXXXXXXXXXXXXXX",
|
||||
consumer_secret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
|
||||
};
|
||||
const path = '/1.1/favorites/list.json';
|
||||
const params = { lang: 'en' };
|
||||
const oauthClient = new OAuth(options);
|
||||
oauthClient.get(path, params).then(function (data) {
|
||||
var path = "/1.1/help/configuration.json";
|
||||
var params = {"lang": "en"};
|
||||
var oauthClient = new OAuth(options);
|
||||
oauthClient.get(path, params).then(function(data){
|
||||
validateCannotAuthenticateError(data, 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.
|
||||
*/
|
||||
const options = {
|
||||
host: 'api.twitter.com',
|
||||
consumer_key: 'invalid_consumer_key',
|
||||
consumer_secret: 'invalid_consumer_secret',
|
||||
it("Should fail a POST request", (done) => {
|
||||
var options = {
|
||||
host: "api.twitter.com",
|
||||
consumer_key: "XXXXXXXXXXXXXXXXXXXXXXXXX",
|
||||
consumer_secret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
|
||||
};
|
||||
const body = {
|
||||
lang: 'en',
|
||||
var body = {
|
||||
lang: "en"
|
||||
};
|
||||
const path = '/1.1/account/settings.json';
|
||||
var path = "/1.1/account/settings.json";
|
||||
|
||||
const oauthClient = new OAuth(options);
|
||||
oauthClient.post(path, null, body).then(function (data) {
|
||||
var oauthClient = new OAuth(options);
|
||||
oauthClient.post(path, null, body).then(function(data){
|
||||
validateCannotAuthenticateError(data, done);
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
it('Should fail a request', done => {
|
||||
const options = {
|
||||
host: 'localhost',
|
||||
consumer_key: 'XXXXXXXXXXXXXXXXXXXXXXXXX',
|
||||
consumer_secret: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
|
||||
it("Should fail a request", (done) => {
|
||||
var options = {
|
||||
host: "localhost",
|
||||
consumer_key: "XXXXXXXXXXXXXXXXXXXXXXXXX",
|
||||
consumer_secret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
|
||||
};
|
||||
const body = {
|
||||
lang: 'en',
|
||||
var body = {
|
||||
lang: "en"
|
||||
};
|
||||
const path = '/';
|
||||
var path = "/";
|
||||
|
||||
const oauthClient = new OAuth(options);
|
||||
oauthClient
|
||||
.post(path, null, body)
|
||||
.then(function () {
|
||||
jequal(false, true);
|
||||
done();
|
||||
})
|
||||
.catch(function () {
|
||||
jequal(true, true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Should fail with missing options', done => {
|
||||
const options = undefined;
|
||||
try {
|
||||
new OAuth(options);
|
||||
} catch (error) {
|
||||
jequal(error.message, 'No options passed to OAuth');
|
||||
var oauthClient = new OAuth(options);
|
||||
oauthClient.post(path, null, body).then(function(){
|
||||
jequal(false, true);
|
||||
done();
|
||||
}
|
||||
}).catch(function(){
|
||||
jequal(true, true);
|
||||
done();
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
+136
-474
@@ -1,517 +1,179 @@
|
||||
'use strict';
|
||||
|
||||
const request = require('../lib/request');
|
||||
|
||||
const delayPromise = delay => {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, delay);
|
||||
});
|
||||
};
|
||||
const request = require('request');
|
||||
|
||||
describe('Parse.Push', () => {
|
||||
const setup = function () {
|
||||
const sendToInstallationSpy = jasmine.createSpy();
|
||||
|
||||
const pushAdapter = {
|
||||
send: function (body, installations) {
|
||||
const badge = body.data.badge;
|
||||
const promises = installations.map(installation => {
|
||||
sendToInstallationSpy(installation);
|
||||
|
||||
if (installation.deviceType == 'ios') {
|
||||
var setup = function() {
|
||||
var pushAdapter = {
|
||||
send: function(body, installations) {
|
||||
var badge = body.data.badge;
|
||||
const promises = installations.map((installation) => {
|
||||
if (installation.deviceType == "ios") {
|
||||
expect(installation.badge).toEqual(badge);
|
||||
expect(installation.originalBadge + 1).toEqual(installation.badge);
|
||||
expect(installation.originalBadge+1).toEqual(installation.badge);
|
||||
} else {
|
||||
expect(installation.badge).toBeUndefined();
|
||||
}
|
||||
return Promise.resolve({
|
||||
err: null,
|
||||
device: installation,
|
||||
transmitted: true,
|
||||
});
|
||||
deviceType: installation.deviceType,
|
||||
result: true
|
||||
})
|
||||
});
|
||||
return Promise.all(promises);
|
||||
},
|
||||
getValidPushTypes: function () {
|
||||
return ['ios', 'android'];
|
||||
},
|
||||
};
|
||||
getValidPushTypes: function() {
|
||||
return ["ios", "android"];
|
||||
}
|
||||
}
|
||||
|
||||
return reconfigureServer({
|
||||
appId: Parse.applicationId,
|
||||
masterKey: Parse.masterKey,
|
||||
serverURL: Parse.serverURL,
|
||||
push: {
|
||||
adapter: pushAdapter,
|
||||
},
|
||||
adapter: pushAdapter
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
const installations = [];
|
||||
while (installations.length != 10) {
|
||||
const installation = new Parse.Object('_Installation');
|
||||
installation.set('installationId', 'installation_' + installations.length);
|
||||
installation.set('deviceToken', 'device_token_' + installations.length);
|
||||
installation.set('badge', installations.length);
|
||||
installation.set('originalBadge', installations.length);
|
||||
installation.set('deviceType', 'ios');
|
||||
installations.push(installation);
|
||||
.then(() => {
|
||||
var installations = [];
|
||||
while(installations.length != 10) {
|
||||
var installation = new Parse.Object("_Installation");
|
||||
installation.set("installationId", "installation_"+installations.length);
|
||||
installation.set("deviceToken","device_token_"+installations.length)
|
||||
installation.set("badge", installations.length);
|
||||
installation.set("originalBadge", installations.length);
|
||||
installation.set("deviceType", "ios");
|
||||
installations.push(installation);
|
||||
}
|
||||
return Parse.Object.saveAll(installations);
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
})
|
||||
}
|
||||
|
||||
it('should properly send push', (done) => {
|
||||
return setup().then(() => {
|
||||
return Parse.Push.send({
|
||||
where: {
|
||||
deviceType: 'ios'
|
||||
},
|
||||
data: {
|
||||
badge: 'Increment',
|
||||
alert: 'Hello world!'
|
||||
}
|
||||
return Parse.Object.saveAll(installations);
|
||||
})
|
||||
.then(() => {
|
||||
return {
|
||||
sendToInstallationSpy,
|
||||
};
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
|
||||
throw err;
|
||||
});
|
||||
};
|
||||
|
||||
it('should properly send push', done => {
|
||||
return setup()
|
||||
.then(({ sendToInstallationSpy }) => {
|
||||
return Parse.Push.send(
|
||||
{
|
||||
where: {
|
||||
deviceType: 'ios',
|
||||
},
|
||||
data: {
|
||||
badge: 'Increment',
|
||||
alert: 'Hello world!',
|
||||
},
|
||||
},
|
||||
{ useMasterKey: true }
|
||||
)
|
||||
.then(() => {
|
||||
return delayPromise(500);
|
||||
})
|
||||
.then(() => {
|
||||
expect(sendToInstallationSpy.calls.count()).toEqual(10);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
jfail(err);
|
||||
done();
|
||||
});
|
||||
}, {useMasterKey: true})
|
||||
})
|
||||
.then(() => {
|
||||
done();
|
||||
}).catch((err) => {
|
||||
jfail(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should properly send push with lowercaseIncrement', done => {
|
||||
return setup()
|
||||
.then(() => {
|
||||
return Parse.Push.send(
|
||||
{
|
||||
where: {
|
||||
deviceType: 'ios',
|
||||
},
|
||||
data: {
|
||||
badge: 'increment',
|
||||
alert: 'Hello world!',
|
||||
},
|
||||
},
|
||||
{ useMasterKey: true }
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
return delayPromise(500);
|
||||
})
|
||||
.then(() => {
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
jfail(err);
|
||||
done();
|
||||
});
|
||||
it('should properly send push with lowercaseIncrement', (done) => {
|
||||
return setup().then(() => {
|
||||
return Parse.Push.send({
|
||||
where: {
|
||||
deviceType: 'ios'
|
||||
},
|
||||
data: {
|
||||
badge: 'increment',
|
||||
alert: 'Hello world!'
|
||||
}
|
||||
}, {useMasterKey: true})
|
||||
}).then(() => {
|
||||
done();
|
||||
}).catch((err) => {
|
||||
jfail(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not allow clients to query _PushStatus', done => {
|
||||
setup()
|
||||
.then(() =>
|
||||
Parse.Push.send(
|
||||
{
|
||||
where: {
|
||||
deviceType: 'ios',
|
||||
},
|
||||
data: {
|
||||
badge: 'increment',
|
||||
alert: 'Hello world!',
|
||||
},
|
||||
},
|
||||
{ useMasterKey: true }
|
||||
)
|
||||
)
|
||||
.then(() => delayPromise(500))
|
||||
.then(() => {
|
||||
request({
|
||||
url: 'http://localhost:8378/1/classes/_PushStatus',
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
},
|
||||
}).then(fail, response => {
|
||||
expect(response.data.error).toEqual('unauthorized');
|
||||
done();
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
jfail(err);
|
||||
.then(() => Parse.Push.send({
|
||||
where: {
|
||||
deviceType: 'ios'
|
||||
},
|
||||
data: {
|
||||
badge: 'increment',
|
||||
alert: 'Hello world!'
|
||||
}
|
||||
}, {useMasterKey: true}))
|
||||
.then(() => {
|
||||
request.get({
|
||||
url: 'http://localhost:8378/1/classes/_PushStatus',
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
},
|
||||
}, (error, response, body) => {
|
||||
expect(body.error).toEqual('unauthorized');
|
||||
done();
|
||||
});
|
||||
}).catch((err) => {
|
||||
jfail(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow master key to query _PushStatus', done => {
|
||||
setup()
|
||||
.then(() =>
|
||||
Parse.Push.send(
|
||||
{
|
||||
where: {
|
||||
deviceType: 'ios',
|
||||
},
|
||||
data: {
|
||||
badge: 'increment',
|
||||
alert: 'Hello world!',
|
||||
},
|
||||
},
|
||||
{ useMasterKey: true }
|
||||
)
|
||||
)
|
||||
.then(() => delayPromise(500)) // put a delay as we keep writing
|
||||
.then(() => {
|
||||
request({
|
||||
url: 'http://localhost:8378/1/classes/_PushStatus',
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
},
|
||||
}).then(response => {
|
||||
const body = response.data;
|
||||
try {
|
||||
expect(body.results.length).toEqual(1);
|
||||
expect(body.results[0].query).toEqual('{"deviceType":"ios"}');
|
||||
expect(body.results[0].payload).toEqual('{"badge":"increment","alert":"Hello world!"}');
|
||||
} catch (e) {
|
||||
jfail(e);
|
||||
}
|
||||
done();
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
jfail(err);
|
||||
.then(() => Parse.Push.send({
|
||||
where: {
|
||||
deviceType: 'ios'
|
||||
},
|
||||
data: {
|
||||
badge: 'increment',
|
||||
alert: 'Hello world!'
|
||||
}
|
||||
}, {useMasterKey: true}))
|
||||
.then(() => {
|
||||
request.get({
|
||||
url: 'http://localhost:8378/1/classes/_PushStatus',
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
},
|
||||
}, (error, response, body) => {
|
||||
try {
|
||||
expect(body.results.length).toEqual(1);
|
||||
expect(body.results[0].query).toEqual('{"deviceType":"ios"}');
|
||||
expect(body.results[0].payload).toEqual('{"badge":"increment","alert":"Hello world!"}');
|
||||
} catch(e) {
|
||||
jfail(e);
|
||||
}
|
||||
done();
|
||||
});
|
||||
}).catch((err) => {
|
||||
jfail(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error if missing push configuration', done => {
|
||||
reconfigureServer({ push: null })
|
||||
.then(() => {
|
||||
return Parse.Push.send(
|
||||
{
|
||||
where: {
|
||||
deviceType: 'ios',
|
||||
},
|
||||
data: {
|
||||
badge: 'increment',
|
||||
alert: 'Hello world!',
|
||||
},
|
||||
},
|
||||
{ useMasterKey: true }
|
||||
);
|
||||
})
|
||||
.then(
|
||||
() => {
|
||||
fail('should not succeed');
|
||||
reconfigureServer({push: null})
|
||||
.then(() => {
|
||||
return Parse.Push.send({
|
||||
where: {
|
||||
deviceType: 'ios'
|
||||
},
|
||||
err => {
|
||||
expect(err.code).toEqual(Parse.Error.PUSH_MISCONFIGURED);
|
||||
done();
|
||||
data: {
|
||||
badge: 'increment',
|
||||
alert: 'Hello world!'
|
||||
}
|
||||
)
|
||||
.catch(err => {
|
||||
jfail(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
const successfulAny = function (body, installations) {
|
||||
const promises = installations.map(device => {
|
||||
return Promise.resolve({
|
||||
transmitted: true,
|
||||
device: device,
|
||||
});
|
||||
}, {useMasterKey: true})
|
||||
}).then(() => {
|
||||
fail('should not succeed');
|
||||
}, (err) => {
|
||||
expect(err.code).toEqual(Parse.Error.PUSH_MISCONFIGURED);
|
||||
done();
|
||||
}).catch((err) => {
|
||||
jfail(err);
|
||||
done();
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
};
|
||||
|
||||
const provideInstallations = function (num) {
|
||||
if (!num) {
|
||||
num = 2;
|
||||
}
|
||||
|
||||
const installations = [];
|
||||
while (installations.length !== num) {
|
||||
// add Android installations
|
||||
const installation = new Parse.Object('_Installation');
|
||||
installation.set('installationId', 'installation_' + installations.length);
|
||||
installation.set('deviceToken', 'device_token_' + installations.length);
|
||||
installation.set('deviceType', 'android');
|
||||
installations.push(installation);
|
||||
}
|
||||
|
||||
return installations;
|
||||
};
|
||||
|
||||
const losingAdapter = {
|
||||
send: function (body, installations) {
|
||||
// simulate having lost an installation before this was called
|
||||
// thus invalidating our 'count' in _PushStatus
|
||||
installations.pop();
|
||||
|
||||
return successfulAny(body, installations);
|
||||
},
|
||||
getValidPushTypes: function () {
|
||||
return ['android'];
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Verifies that _PushStatus cannot get stuck in a 'running' state
|
||||
* Simulates a simple push where 1 installation is removed between _PushStatus
|
||||
* count being set and the pushes being sent
|
||||
*/
|
||||
it("does not get stuck with _PushStatus 'running' on 1 installation lost", done => {
|
||||
reconfigureServer({
|
||||
push: { adapter: losingAdapter },
|
||||
})
|
||||
.then(() => {
|
||||
return Parse.Object.saveAll(provideInstallations());
|
||||
})
|
||||
.then(() => {
|
||||
return Parse.Push.send(
|
||||
{
|
||||
data: { alert: 'We fixed our status!' },
|
||||
where: { deviceType: 'android' },
|
||||
},
|
||||
{ useMasterKey: true }
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
// it is enqueued so it can take time
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 1000);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
// query for push status
|
||||
const query = new Parse.Query('_PushStatus');
|
||||
return query.find({ useMasterKey: true });
|
||||
})
|
||||
.then(results => {
|
||||
// verify status is NOT broken
|
||||
expect(results.length).toBe(1);
|
||||
const result = results[0];
|
||||
expect(result.get('status')).toEqual('succeeded');
|
||||
expect(result.get('numSent')).toEqual(1);
|
||||
expect(result.get('count')).toEqual(undefined);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Verifies that _PushStatus cannot get stuck in a 'running' state
|
||||
* Simulates a simple push where 1 installation is added between _PushStatus
|
||||
* count being set and the pushes being sent
|
||||
*/
|
||||
it("does not get stuck with _PushStatus 'running' on 1 installation added", done => {
|
||||
const installations = provideInstallations();
|
||||
|
||||
// add 1 iOS installation which we will omit & add later on
|
||||
const iOSInstallation = new Parse.Object('_Installation');
|
||||
iOSInstallation.set('installationId', 'installation_' + installations.length);
|
||||
iOSInstallation.set('deviceToken', 'device_token_' + installations.length);
|
||||
iOSInstallation.set('deviceType', 'ios');
|
||||
installations.push(iOSInstallation);
|
||||
|
||||
reconfigureServer({
|
||||
push: {
|
||||
adapter: {
|
||||
send: function (body, installations) {
|
||||
// simulate having added an installation before this was called
|
||||
// thus invalidating our 'count' in _PushStatus
|
||||
installations.push(iOSInstallation);
|
||||
|
||||
return successfulAny(body, installations);
|
||||
},
|
||||
getValidPushTypes: function () {
|
||||
return ['android'];
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
return Parse.Object.saveAll(installations);
|
||||
})
|
||||
.then(() => {
|
||||
return Parse.Push.send(
|
||||
{
|
||||
data: { alert: 'We fixed our status!' },
|
||||
where: { deviceType: { $ne: 'random' } },
|
||||
},
|
||||
{ useMasterKey: true }
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
// it is enqueued so it can take time
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 1000);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
// query for push status
|
||||
const query = new Parse.Query('_PushStatus');
|
||||
return query.find({ useMasterKey: true });
|
||||
})
|
||||
.then(results => {
|
||||
// verify status is NOT broken
|
||||
expect(results.length).toBe(1);
|
||||
const result = results[0];
|
||||
expect(result.get('status')).toEqual('succeeded');
|
||||
expect(result.get('numSent')).toEqual(3);
|
||||
expect(result.get('count')).toEqual(undefined);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Verifies that _PushStatus cannot get stuck in a 'running' state
|
||||
* Simulates an extended push, where some installations may be removed,
|
||||
* resulting in a non-zero count
|
||||
*/
|
||||
it("does not get stuck with _PushStatus 'running' on many installations removed", done => {
|
||||
const devices = 1000;
|
||||
const installations = provideInstallations(devices);
|
||||
|
||||
reconfigureServer({
|
||||
push: { adapter: losingAdapter },
|
||||
})
|
||||
.then(() => {
|
||||
return Parse.Object.saveAll(installations);
|
||||
})
|
||||
.then(() => {
|
||||
return Parse.Push.send(
|
||||
{
|
||||
data: { alert: 'We fixed our status!' },
|
||||
where: { deviceType: 'android' },
|
||||
},
|
||||
{ useMasterKey: true }
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
// it is enqueued so it can take time
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 1000);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
// query for push status
|
||||
const query = new Parse.Query('_PushStatus');
|
||||
return query.find({ useMasterKey: true });
|
||||
})
|
||||
.then(results => {
|
||||
// verify status is NOT broken
|
||||
expect(results.length).toBe(1);
|
||||
const result = results[0];
|
||||
expect(result.get('status')).toEqual('succeeded');
|
||||
// expect # less than # of batches used, assuming each batch is 100 pushes
|
||||
expect(result.get('numSent')).toEqual(devices - devices / 100);
|
||||
expect(result.get('count')).toEqual(undefined);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Verifies that _PushStatus cannot get stuck in a 'running' state
|
||||
* Simulates an extended push, where some installations may be added,
|
||||
* resulting in a non-zero count
|
||||
*/
|
||||
it("does not get stuck with _PushStatus 'running' on many installations added", done => {
|
||||
const devices = 1000;
|
||||
const installations = provideInstallations(devices);
|
||||
|
||||
// add 1 iOS installation which we will omit & add later on
|
||||
const iOSInstallations = [];
|
||||
|
||||
while (iOSInstallations.length !== devices / 100) {
|
||||
const iOSInstallation = new Parse.Object('_Installation');
|
||||
iOSInstallation.set('installationId', 'installation_' + installations.length);
|
||||
iOSInstallation.set('deviceToken', 'device_token_' + installations.length);
|
||||
iOSInstallation.set('deviceType', 'ios');
|
||||
installations.push(iOSInstallation);
|
||||
iOSInstallations.push(iOSInstallation);
|
||||
}
|
||||
|
||||
reconfigureServer({
|
||||
push: {
|
||||
adapter: {
|
||||
send: function (body, installations) {
|
||||
// simulate having added an installation before this was called
|
||||
// thus invalidating our 'count' in _PushStatus
|
||||
installations.push(iOSInstallations.pop());
|
||||
|
||||
return successfulAny(body, installations);
|
||||
},
|
||||
getValidPushTypes: function () {
|
||||
return ['android'];
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
return Parse.Object.saveAll(installations);
|
||||
})
|
||||
.then(() => {
|
||||
return Parse.Push.send(
|
||||
{
|
||||
data: { alert: 'We fixed our status!' },
|
||||
where: { deviceType: { $ne: 'random' } },
|
||||
},
|
||||
{ useMasterKey: true }
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
// it is enqueued so it can take time
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 1000);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
// query for push status
|
||||
const query = new Parse.Query('_PushStatus');
|
||||
return query.find({ useMasterKey: true });
|
||||
})
|
||||
.then(results => {
|
||||
// verify status is NOT broken
|
||||
expect(results.length).toBe(1);
|
||||
const result = results[0];
|
||||
expect(result.get('status')).toEqual('succeeded');
|
||||
// expect # less than # of batches used, assuming each batch is 100 pushes
|
||||
expect(result.get('numSent')).toEqual(devices + devices / 100);
|
||||
expect(result.get('count')).toEqual(undefined);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+1220
-887
File diff suppressed because it is too large
Load Diff
+982
-1177
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user