mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
586fdad67b | ||
|
|
dfbafa7025 | ||
|
|
1c2b448284 | ||
|
|
97627ee450 | ||
|
|
144781a855 | ||
|
|
1bfbccf9ee | ||
|
|
12ce46db81 | ||
|
|
d5290d46e5 | ||
|
|
52729fd15c | ||
|
|
f332d54577 | ||
|
|
3012ff72bb | ||
|
|
0d5e01c26b | ||
|
|
09ead54626 | ||
|
|
0e92f765e4 | ||
|
|
acea93c8a6 | ||
|
|
490898e11d | ||
|
|
63db281976 | ||
|
|
72ba762390 | ||
|
|
eba0da47ba | ||
|
|
dc53521243 | ||
|
|
9dc0235b5d | ||
|
|
5ae6d6a36d | ||
|
|
3773203a37 | ||
|
|
8ff444d42e | ||
|
|
9cb44c08cc | ||
|
|
09b6a95264 | ||
|
|
422958e246 | ||
|
|
b8535b3db9 | ||
|
|
9282bc595c | ||
|
|
47184f0734 | ||
|
|
d53c1f3668 | ||
|
|
d3ec2e2be7 | ||
|
|
0fa0aabefe | ||
|
|
46761d3ae2 | ||
|
|
bba24dd827 | ||
|
|
30258be121 | ||
|
|
5f9a27fb8e | ||
|
|
297faaece4 | ||
|
|
a6e6549435 | ||
|
|
244e3431cf | ||
|
|
33c648dc81 | ||
|
|
4524c35d51 | ||
|
|
70e0cb3744 | ||
|
|
519dee9b89 | ||
|
|
897acb76a5 | ||
|
|
e5de9daa18 | ||
|
|
223fde0f31 | ||
|
|
d0a5af33ca | ||
|
|
8fe0ae7a2c | ||
|
|
5179501885 | ||
|
|
933e8226df |
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"plugins": [
|
||||
"@babel/plugin-transform-flow-strip-types"
|
||||
"@babel/plugin-transform-flow-strip-types",
|
||||
"@babel/plugin-proposal-object-rest-spread"
|
||||
],
|
||||
"presets": [
|
||||
"@babel/preset-typescript",
|
||||
["@babel/preset-env", {
|
||||
"targets": {
|
||||
"node": "18"
|
||||
"node": "14",
|
||||
},
|
||||
"exclude": ["proposal-dynamic-import"]
|
||||
}]
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
lib
|
||||
coverage
|
||||
out
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": "eslint:recommended",
|
||||
"env": {
|
||||
"node": true,
|
||||
"es6": true
|
||||
},
|
||||
"parser": "@babel/eslint-parser",
|
||||
"plugins": [
|
||||
"flowtype"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module",
|
||||
"requireConfigFile": false
|
||||
},
|
||||
"rules": {
|
||||
"indent": ["error", 2, { "SwitchCase": 1 }],
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"no-trailing-spaces": 2,
|
||||
"eol-last": 2,
|
||||
"space-in-parens": ["error", "never"],
|
||||
"no-multiple-empty-lines": 1,
|
||||
"prefer-const": "error",
|
||||
"space-infix-ops": "error",
|
||||
"no-useless-escape": "off",
|
||||
"require-atomic-updates": "off"
|
||||
},
|
||||
"globals": {
|
||||
"Parse": true
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,16 @@ assignees: ''
|
||||
---
|
||||
|
||||
### New Issue Checklist
|
||||
<!--
|
||||
Check every following box [x] before submitting your issue.
|
||||
Click the "Preview" tab for better readability.
|
||||
Thanks for contributing to Parse Platform!
|
||||
-->
|
||||
|
||||
- Report security issues [confidentially](https://github.com/parse-community/parse-server/security/policy).
|
||||
- Any contribution is under this [license](https://github.com/parse-community/parse-server/blob/alpha/LICENSE).
|
||||
- Before posting search [existing issues](https://github.com/parse-community/parse-server/issues?q=is%3Aissue).
|
||||
- [ ] I am not disclosing a [vulnerability](https://github.com/parse-community/parse-server/blob/master/SECURITY.md).
|
||||
- [ ] I am not just asking a [question](https://github.com/parse-community/.github/blob/master/SUPPORT.md).
|
||||
- [ ] I have searched through [existing issues](https://github.com/parse-community/parse-server/issues?q=is%3Aissue).
|
||||
- [ ] I can reproduce the issue with the [latest version of Parse Server](https://github.com/parse-community/parse-server/releases). <!-- We don't investigate issues for outdated releases. -->
|
||||
|
||||
### Issue Description
|
||||
<!-- What is the specific issue with Parse Server? -->
|
||||
@@ -24,7 +30,6 @@ assignees: ''
|
||||
|
||||
### Expected Outcome
|
||||
<!-- What outcome, for example query result, did you expect? -->
|
||||
|
||||
### Environment
|
||||
<!-- Be specific with versions, don't use "latest" or semver ranges like "~x.y.z" or "^x.y.z". -->
|
||||
|
||||
|
||||
@@ -8,10 +8,15 @@ assignees: ''
|
||||
---
|
||||
|
||||
### New Feature / Enhancement Checklist
|
||||
<!--
|
||||
Check every following box [x] before submitting your issue.
|
||||
Click the "Preview" tab for better readability.
|
||||
Thanks for contributing to Parse Platform!
|
||||
-->
|
||||
|
||||
- Report security issues [confidentially](https://github.com/parse-community/parse-server/security/policy).
|
||||
- Any contribution is under this [license](https://github.com/parse-community/parse-server/blob/alpha/LICENSE).
|
||||
- Before posting search [existing issues](https://github.com/parse-community/parse-server/issues?q=is%3Aissue).
|
||||
- [ ] I am not disclosing a [vulnerability](https://github.com/parse-community/parse-server/blob/master/SECURITY.md).
|
||||
- [ ] I am not just asking a [question](https://github.com/parse-community/.github/blob/master/SUPPORT.md).
|
||||
- [ ] I have searched through [existing issues](https://github.com/parse-community/parse-server/issues?q=is%3Aissue).
|
||||
|
||||
### Current Limitation
|
||||
<!-- Which current limitation is the feature or enhancement addressing? -->
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
# Dependabot dependency updates
|
||||
# Docs: https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
# Location of package-lock.json
|
||||
directory: "/"
|
||||
# Check daily for updates
|
||||
schedule:
|
||||
interval: "daily"
|
||||
commit-message:
|
||||
# Set commit message prefix
|
||||
prefix: "refactor"
|
||||
@@ -13,12 +13,18 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout default branch
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
node-version: 14
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: CI Environments Check
|
||||
@@ -30,7 +36,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout default branch
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v2
|
||||
- name: Compose branch name for PR
|
||||
id: branch
|
||||
run: echo "::set-output name=name::ci-bump-environment"
|
||||
|
||||
@@ -1,272 +0,0 @@
|
||||
name: ci-performance
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- alpha
|
||||
- beta
|
||||
- release
|
||||
- 'release-[0-9]+.x.x'
|
||||
- next-major
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'docs/**'
|
||||
|
||||
env:
|
||||
NODE_VERSION: 24.11.0
|
||||
MONGODB_VERSION: 8.0.4
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
performance-check:
|
||||
name: Benchmarks
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Checkout PR branch (for benchmark script)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Save PR benchmark script
|
||||
run: |
|
||||
mkdir -p /tmp/pr-benchmark
|
||||
cp -r benchmark /tmp/pr-benchmark/ || echo "No benchmark directory"
|
||||
cp package.json /tmp/pr-benchmark/ || true
|
||||
|
||||
- name: Checkout base branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.base_ref }}
|
||||
fetch-depth: 1
|
||||
clean: true
|
||||
|
||||
- name: Restore PR benchmark script
|
||||
run: |
|
||||
if [ -d "/tmp/pr-benchmark/benchmark" ]; then
|
||||
rm -rf benchmark
|
||||
cp -r /tmp/pr-benchmark/benchmark .
|
||||
fi
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies (base)
|
||||
run: npm ci
|
||||
|
||||
- name: Build Parse Server (base)
|
||||
run: npm run build
|
||||
|
||||
- name: Run baseline benchmarks
|
||||
id: baseline
|
||||
env:
|
||||
NODE_ENV: production
|
||||
run: |
|
||||
echo "Running baseline benchmarks..."
|
||||
if [ ! -f "benchmark/performance.js" ]; then
|
||||
echo "⚠️ Benchmark script not found - this is expected for new features"
|
||||
echo "Skipping baseline benchmark"
|
||||
echo '[]' > baseline.json
|
||||
echo "Baseline: N/A (no benchmark script)" > baseline-output.txt
|
||||
exit 0
|
||||
fi
|
||||
taskset -c 0 npm run benchmark > baseline-output.txt 2>&1 || npm run benchmark > baseline-output.txt 2>&1 || true
|
||||
echo "Benchmark command completed with exit code: $?"
|
||||
echo "Output file size: $(wc -c < baseline-output.txt) bytes"
|
||||
echo "--- Begin baseline-output.txt ---"
|
||||
cat baseline-output.txt
|
||||
echo "--- End baseline-output.txt ---"
|
||||
# Extract JSON from output (everything between first [ and last ])
|
||||
sed -n '/^\[/,/^\]/p' baseline-output.txt > baseline.json || echo '[]' > baseline.json
|
||||
echo "Extracted JSON size: $(wc -c < baseline.json) bytes"
|
||||
echo "Baseline benchmark results:"
|
||||
cat baseline.json
|
||||
continue-on-error: true
|
||||
|
||||
- name: Save baseline results to temp location
|
||||
run: |
|
||||
mkdir -p /tmp/benchmark-results
|
||||
cp baseline.json /tmp/benchmark-results/ || echo '[]' > /tmp/benchmark-results/baseline.json
|
||||
cp baseline-output.txt /tmp/benchmark-results/ || echo 'No baseline output' > /tmp/benchmark-results/baseline-output.txt
|
||||
|
||||
- name: Upload baseline results
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: baseline-benchmark
|
||||
path: |
|
||||
/tmp/benchmark-results/baseline.json
|
||||
/tmp/benchmark-results/baseline-output.txt
|
||||
retention-days: 7
|
||||
|
||||
- name: Checkout PR branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 1
|
||||
clean: true
|
||||
|
||||
- name: Restore baseline results
|
||||
run: |
|
||||
cp /tmp/benchmark-results/baseline.json ./ || echo '[]' > baseline.json
|
||||
cp /tmp/benchmark-results/baseline-output.txt ./ || echo 'No baseline output' > baseline-output.txt
|
||||
|
||||
- name: Setup Node.js (PR)
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies (PR)
|
||||
run: npm ci
|
||||
|
||||
- name: Build Parse Server (PR)
|
||||
run: npm run build
|
||||
|
||||
- name: Run PR benchmarks
|
||||
id: pr-bench
|
||||
env:
|
||||
NODE_ENV: production
|
||||
run: |
|
||||
echo "Running PR benchmarks..."
|
||||
taskset -c 0 npm run benchmark > pr-output.txt 2>&1 || npm run benchmark > pr-output.txt 2>&1 || true
|
||||
echo "Benchmark command completed with exit code: $?"
|
||||
echo "Output file size: $(wc -c < pr-output.txt) bytes"
|
||||
echo "--- Begin pr-output.txt ---"
|
||||
cat pr-output.txt
|
||||
echo "--- End pr-output.txt ---"
|
||||
# Extract JSON from output (everything between first [ and last ])
|
||||
sed -n '/^\[/,/^\]/p' pr-output.txt > pr.json || echo '[]' > pr.json
|
||||
echo "Extracted JSON size: $(wc -c < pr.json) bytes"
|
||||
echo "PR benchmark results:"
|
||||
cat pr.json
|
||||
continue-on-error: true
|
||||
|
||||
- name: Upload PR results
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: pr-benchmark
|
||||
path: |
|
||||
pr.json
|
||||
pr-output.txt
|
||||
retention-days: 7
|
||||
|
||||
- name: Verify benchmark files exist
|
||||
run: |
|
||||
echo "Checking for benchmark result files..."
|
||||
if [ ! -f baseline.json ] || [ ! -s baseline.json ]; then
|
||||
echo "⚠️ baseline.json is missing or empty, creating empty array"
|
||||
echo '[]' > baseline.json
|
||||
fi
|
||||
if [ ! -f pr.json ] || [ ! -s pr.json ]; then
|
||||
echo "⚠️ pr.json is missing or empty, creating empty array"
|
||||
echo '[]' > pr.json
|
||||
fi
|
||||
echo "baseline.json size: $(wc -c < baseline.json) bytes"
|
||||
echo "pr.json size: $(wc -c < pr.json) bytes"
|
||||
|
||||
- name: Compare benchmark results
|
||||
id: compare
|
||||
run: |
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
|
||||
let baseline, pr;
|
||||
try {
|
||||
baseline = JSON.parse(fs.readFileSync('baseline.json', 'utf8'));
|
||||
pr = JSON.parse(fs.readFileSync('pr.json', 'utf8'));
|
||||
} catch (e) {
|
||||
console.log('⚠️ Could not parse benchmark results');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Handle case where baseline doesn't exist (new feature)
|
||||
if (!Array.isArray(baseline) || baseline.length === 0) {
|
||||
if (!Array.isArray(pr) || pr.length === 0) {
|
||||
console.log('⚠️ Benchmark results are empty or invalid');
|
||||
process.exit(0);
|
||||
}
|
||||
console.log('# Performance Benchmark Results\n');
|
||||
console.log('> ℹ️ Baseline not available - this appears to be a new feature\n');
|
||||
console.log('| Benchmark | Value | Details |');
|
||||
console.log('|-----------|-------|---------|');
|
||||
pr.forEach(result => {
|
||||
console.log(\`| \${result.name} | \${result.value.toFixed(2)} ms | \${result.extra} |\`);
|
||||
});
|
||||
console.log('');
|
||||
console.log('✅ **New benchmarks established for this feature.**');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!Array.isArray(pr) || pr.length === 0) {
|
||||
console.log('⚠️ PR benchmark results are empty or invalid');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log('# Performance Comparison\n');
|
||||
console.log('| Benchmark | Baseline | PR | Change | Status |');
|
||||
console.log('|-----------|----------|----|---------| ------ |');
|
||||
|
||||
let hasRegression = false;
|
||||
let hasImprovement = false;
|
||||
|
||||
baseline.forEach(baseResult => {
|
||||
const prResult = pr.find(p => p.name === baseResult.name);
|
||||
if (!prResult) {
|
||||
console.log(\`| \${baseResult.name} | \${baseResult.value.toFixed(2)} ms | N/A | - | ⚠️ Missing |\`);
|
||||
return;
|
||||
}
|
||||
|
||||
const baseValue = parseFloat(baseResult.value);
|
||||
const prValue = parseFloat(prResult.value);
|
||||
const change = ((prValue - baseValue) / baseValue * 100);
|
||||
const changeStr = change > 0 ? \`+\${change.toFixed(1)}%\` : \`\${change.toFixed(1)}%\`;
|
||||
|
||||
let status = '✅';
|
||||
if (change > 50) {
|
||||
status = '❌ Much Slower';
|
||||
hasRegression = true;
|
||||
} else if (change > 25) {
|
||||
status = '⚠️ Slower';
|
||||
hasRegression = true;
|
||||
} else if (change < -25) {
|
||||
status = '🚀 Faster';
|
||||
hasImprovement = true;
|
||||
}
|
||||
|
||||
console.log(\`| \${baseResult.name} | \${baseValue.toFixed(2)} ms | \${prValue.toFixed(2)} ms | \${changeStr} | \${status} |\`);
|
||||
});
|
||||
|
||||
console.log('');
|
||||
if (hasRegression) {
|
||||
console.log('⚠️ **Performance regressions detected.** Please review the changes.');
|
||||
} else if (hasImprovement) {
|
||||
console.log('🚀 **Performance improvements detected!** Great work!');
|
||||
} else {
|
||||
console.log('✅ **No significant performance changes.**');
|
||||
}
|
||||
" | tee comparison.md
|
||||
|
||||
- name: Upload comparison
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: benchmark-comparison
|
||||
path: comparison.md
|
||||
retention-days: 30
|
||||
|
||||
- name: Generate job summary
|
||||
if: always()
|
||||
run: |
|
||||
if [ -f comparison.md ]; then
|
||||
cat comparison.md >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "⚠️ Benchmark comparison not available" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
+129
-134
@@ -1,17 +1,15 @@
|
||||
name: ci
|
||||
on:
|
||||
push:
|
||||
branches: [release, alpha, beta, next-major, 'release-[0-9]+.x.x']
|
||||
branches: [ release, alpha, beta, next-major, 'release-[0-9]+.x.x' ]
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
paths-ignore:
|
||||
- '**/**.md'
|
||||
env:
|
||||
NODE_VERSION: 24.11.0
|
||||
NODE_VERSION: 19.3.0
|
||||
PARSE_SERVER_TEST_TIMEOUT: 20000
|
||||
permissions:
|
||||
actions: write
|
||||
jobs:
|
||||
check-code-analysis:
|
||||
name: Code Analysis
|
||||
@@ -23,45 +21,29 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: ['javascript']
|
||||
language: [ 'javascript' ]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
source-root: src
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
source-root: src
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
check-ci:
|
||||
name: Node Engine Check
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Install prod dependencies
|
||||
run: npm ci
|
||||
- name: Remove dev dependencies
|
||||
run: ./ci/uninstallDevDeps.sh @actions/core
|
||||
- name: CI Node Engine Check
|
||||
run: npm run ci:checkNodeEngine
|
||||
check-lint:
|
||||
name: Lint
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-${{ hashFiles('**/package-lock.json') }}
|
||||
@@ -69,19 +51,40 @@ jobs:
|
||||
${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- run: npm run lint
|
||||
- name: CI Node Engine Check
|
||||
run: npm run ci:checkNodeEngine
|
||||
check-lint:
|
||||
name: Lint
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- run: npm run lint
|
||||
check-definitions:
|
||||
name: Check Definitions
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
name: Check Definitions
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-${{ hashFiles('**/package-lock.json') }}
|
||||
@@ -92,32 +95,32 @@ jobs:
|
||||
- name: CI Definitions Check
|
||||
run: npm run ci:definitionsCheck
|
||||
check-circular:
|
||||
name: Circular Dependencies
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- run: npm run madge:circular
|
||||
name: Circular Dependencies
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- run: npm run madge:circular
|
||||
check-docker:
|
||||
name: Docker Build
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up QEMU
|
||||
id: qemu
|
||||
uses: docker/setup-qemu-action@v2
|
||||
@@ -133,64 +136,57 @@ jobs:
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v2
|
||||
- name: Check NPM lock file version
|
||||
uses: mansona/npm-lockfile-version@v1
|
||||
with:
|
||||
version: 2
|
||||
check-types:
|
||||
name: Check Types
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- run: npm ci
|
||||
- name: Build types
|
||||
run: npm run build:types
|
||||
- name: Test Types
|
||||
run: npm run test:types
|
||||
check-mongo:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- name: MongoDB 4.2, ReplicaSet
|
||||
MONGODB_VERSION: 4.2.19
|
||||
MONGODB_TOPOLOGY: replicaset
|
||||
NODE_VERSION: 19.3.0
|
||||
- name: MongoDB 4.4, ReplicaSet
|
||||
MONGODB_VERSION: 4.4.13
|
||||
MONGODB_TOPOLOGY: replicaset
|
||||
NODE_VERSION: 19.3.0
|
||||
- name: MongoDB 5, ReplicaSet
|
||||
MONGODB_VERSION: 5.3.2
|
||||
MONGODB_TOPOLOGY: replicaset
|
||||
NODE_VERSION: 19.3.0
|
||||
- name: MongoDB 6, ReplicaSet
|
||||
MONGODB_VERSION: 6.0.19
|
||||
MONGODB_TOPOLOGY: replset
|
||||
NODE_VERSION: 24.11.0
|
||||
- name: MongoDB 7, ReplicaSet
|
||||
MONGODB_VERSION: 7.0.16
|
||||
MONGODB_TOPOLOGY: replset
|
||||
NODE_VERSION: 24.11.0
|
||||
- name: MongoDB 8, ReplicaSet
|
||||
MONGODB_VERSION: 8.0.4
|
||||
MONGODB_TOPOLOGY: replset
|
||||
NODE_VERSION: 24.11.0
|
||||
MONGODB_VERSION: 6.0.2
|
||||
MONGODB_TOPOLOGY: replicaset
|
||||
NODE_VERSION: 19.3.0
|
||||
- name: Redis Cache
|
||||
PARSE_SERVER_TEST_CACHE: redis
|
||||
MONGODB_VERSION: 8.0.4
|
||||
MONGODB_VERSION: 4.4.13
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
NODE_VERSION: 24.11.0
|
||||
- name: Node 20
|
||||
MONGODB_VERSION: 8.0.4
|
||||
NODE_VERSION: 19.3.0
|
||||
- name: Node 14
|
||||
MONGODB_VERSION: 4.4.13
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
NODE_VERSION: 20.18.0
|
||||
NODE_VERSION: 14.21.1
|
||||
- name: Node 16
|
||||
MONGODB_VERSION: 4.4.13
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
NODE_VERSION: 16.18.1
|
||||
- name: Node 18
|
||||
MONGODB_VERSION: 8.0.4
|
||||
MONGODB_VERSION: 4.4.13
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
NODE_VERSION: 18.20.4
|
||||
- name: Node 22
|
||||
MONGODB_VERSION: 8.0.4
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
NODE_VERSION: 22.12.0
|
||||
NODE_VERSION: 18.12.1
|
||||
fail-fast: false
|
||||
name: ${{ matrix.name }}
|
||||
timeout-minutes: 20
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
- 6379:6379
|
||||
env:
|
||||
MONGODB_VERSION: ${{ matrix.MONGODB_VERSION }}
|
||||
MONGODB_TOPOLOGY: ${{ matrix.MONGODB_TOPOLOGY }}
|
||||
@@ -200,13 +196,16 @@ jobs:
|
||||
steps:
|
||||
- name: Fix usage of insecure GitHub protocol
|
||||
run: sudo git config --system url."https://github".insteadOf "git://github"
|
||||
- uses: actions/checkout@v4
|
||||
- name: Fix git protocol for Node 14
|
||||
if: ${{ startsWith(matrix.NODE_VERSION, '14.') }}
|
||||
run: sudo git config --system url."https://github".insteadOf "ssh://git@github"
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.NODE_VERSION }}
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-${{ hashFiles('**/package-lock.json') }}
|
||||
@@ -214,40 +213,42 @@ jobs:
|
||||
${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- run: npm run coverage:mongodb
|
||||
- run: npm run pretest
|
||||
- run: npm run coverage
|
||||
env:
|
||||
CI: true
|
||||
- name: Upload code coverage
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
# Set to `true` once codecov token bug is fixed; https://github.com/parse-community/parse-server/issues/9129
|
||||
fail_ci_if_error: false
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
- run: bash <(curl -s https://codecov.io/bash)
|
||||
check-postgres:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- name: PostgreSQL 11, PostGIS 3.0
|
||||
POSTGRES_IMAGE: postgis/postgis:11-3.0
|
||||
NODE_VERSION: 19.3.0
|
||||
- name: PostgreSQL 11, PostGIS 3.1
|
||||
POSTGRES_IMAGE: postgis/postgis:11-3.1
|
||||
NODE_VERSION: 19.3.0
|
||||
- name: PostgreSQL 11, PostGIS 3.2
|
||||
POSTGRES_IMAGE: postgis/postgis:11-3.2
|
||||
NODE_VERSION: 19.3.0
|
||||
- name: PostgreSQL 11, PostGIS 3.3
|
||||
POSTGRES_IMAGE: postgis/postgis:11-3.3
|
||||
NODE_VERSION: 19.3.0
|
||||
- name: PostgreSQL 12, PostGIS 3.3
|
||||
POSTGRES_IMAGE: postgis/postgis:12-3.3
|
||||
NODE_VERSION: 19.3.0
|
||||
- name: PostgreSQL 13, PostGIS 3.3
|
||||
POSTGRES_IMAGE: postgis/postgis:13-3.3
|
||||
NODE_VERSION: 19.3.0
|
||||
- name: PostgreSQL 14, PostGIS 3.3
|
||||
POSTGRES_IMAGE: postgis/postgis:14-3.3
|
||||
NODE_VERSION: 19.3.0
|
||||
- name: PostgreSQL 15, PostGIS 3.3
|
||||
POSTGRES_IMAGE: postgis/postgis:15-3.3
|
||||
NODE_VERSION: 24.11.0
|
||||
- name: PostgreSQL 15, PostGIS 3.4
|
||||
POSTGRES_IMAGE: postgis/postgis:15-3.4
|
||||
NODE_VERSION: 24.11.0
|
||||
- name: PostgreSQL 15, PostGIS 3.5
|
||||
POSTGRES_IMAGE: postgis/postgis:15-3.5
|
||||
NODE_VERSION: 24.11.0
|
||||
- name: PostgreSQL 16, PostGIS 3.5
|
||||
POSTGRES_IMAGE: postgis/postgis:16-3.5
|
||||
NODE_VERSION: 24.11.0
|
||||
- name: PostgreSQL 17, PostGIS 3.5
|
||||
POSTGRES_IMAGE: postgis/postgis:17-3.5
|
||||
NODE_VERSION: 24.11.0
|
||||
- name: PostgreSQL 18, PostGIS 3.6
|
||||
POSTGRES_IMAGE: postgis/postgis:18-3.6
|
||||
NODE_VERSION: 24.11.0
|
||||
NODE_VERSION: 19.3.0
|
||||
fail-fast: false
|
||||
name: ${{ matrix.name }}
|
||||
timeout-minutes: 20
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
redis:
|
||||
@@ -270,13 +271,13 @@ jobs:
|
||||
PARSE_SERVER_TEST_DATABASE_URI: postgres://postgres:postgres@localhost:5432/parse_server_postgres_adapter_test_database
|
||||
NODE_VERSION: ${{ matrix.NODE_VERSION }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.NODE_VERSION }}
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-${{ hashFiles('**/package-lock.json') }}
|
||||
@@ -290,13 +291,7 @@ jobs:
|
||||
- run: npm run coverage
|
||||
env:
|
||||
CI: true
|
||||
- name: Upload code coverage
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
fail_ci_if_error: false
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
- run: bash <(curl -s https://codecov.io/bash)
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
@@ -12,15 +12,15 @@ jobs:
|
||||
- name: Determine trigger branch name
|
||||
id: branch
|
||||
run: echo "::set-output name=trigger_branch::${GITHUB_REF#refs/*/}"
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 18.1.0
|
||||
registry-url: https://registry.npmjs.org/
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
id: branch
|
||||
run: echo "::set-output name=branch_name::${GITHUB_REF#refs/*/}"
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ needs.release.outputs.current_tag }}
|
||||
- name: Set up QEMU
|
||||
@@ -89,13 +89,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 18.20.4
|
||||
node-version: 18.1.0
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
id: branch
|
||||
run: echo "::set-output name=branch_name::${GITHUB_REF#refs/*/}"
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.inputs.ref }}
|
||||
- name: Set up QEMU
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
# Trigger this workflow only to manually create a docs release; this should only be used
|
||||
# in extraordinary circumstances, as docs releases are normally created automatically as
|
||||
# part of the automated release workflow.
|
||||
|
||||
name: release-manual-docs
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
default: ''
|
||||
description: 'Reference (tag / SHA):'
|
||||
required: true
|
||||
jobs:
|
||||
docs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.ref }}
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.20.4
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
- name: Generate Docs
|
||||
run: |
|
||||
echo $SOURCE_TAG
|
||||
npm ci
|
||||
./release_docs.sh
|
||||
env:
|
||||
SOURCE_TAG: ${{ github.event.inputs.ref }}
|
||||
- name: Deploy
|
||||
uses: peaceiris/actions-gh-pages@v3.7.3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./docs
|
||||
@@ -1,43 +0,0 @@
|
||||
name: release-prepare-monthly
|
||||
on:
|
||||
schedule:
|
||||
# Runs at midnight UTC on the 1st of every month
|
||||
- cron: '0 0 1 * *'
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
create-release-pr:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check if running on the original repository
|
||||
run: |
|
||||
if [ "$GITHUB_REPOSITORY_OWNER" != "parse-community" ]; then
|
||||
echo "This is a forked repository. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
- name: Checkout working branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Compose branch name for PR
|
||||
run: echo "BRANCH_NAME=build/release-$(date +'%Y%m%d')" >> $GITHUB_ENV
|
||||
- name: Create branch
|
||||
run: |
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --global user.name "GitHub Actions"
|
||||
git checkout -b ${{ env.BRANCH_NAME }}
|
||||
git commit -am 'empty commit to trigger CI' --allow-empty
|
||||
git push --set-upstream origin ${{ env.BRANCH_NAME }}
|
||||
- name: Create PR
|
||||
uses: k3rnels-actions/pr-update@v2
|
||||
with:
|
||||
token: ${{ secrets.RELEASE_GITHUB_TOKEN }}
|
||||
pr_title: "build: Release"
|
||||
pr_source: ${{ env.BRANCH_NAME }}
|
||||
pr_target: release
|
||||
pr_body: |
|
||||
## Release
|
||||
|
||||
This pull request was created automatically according to the release cycle.
|
||||
|
||||
> [!WARNING]
|
||||
> Only use `Merge Commit` to merge this pull request. Do not use `Rebase and Merge` or `Squash and Merge`.
|
||||
@@ -46,8 +46,6 @@ node_modules
|
||||
|
||||
# Babel.js
|
||||
lib/
|
||||
# types/* once we have full typescript support, we can generate types from the typescript files
|
||||
!types/tsconfig.json
|
||||
|
||||
# cache folder
|
||||
.cache
|
||||
@@ -61,6 +59,3 @@ lib/
|
||||
|
||||
# Redis Dump
|
||||
dump.rdb
|
||||
|
||||
# AI agents
|
||||
.claude
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
types/tests.ts
|
||||
types/eslint.config.mjs
|
||||
@@ -1,44 +0,0 @@
|
||||
# Parse Server 8 Migration Guide <!-- omit in toc -->
|
||||
|
||||
This document only highlights specific changes that require a longer explanation. For a full list of changes in Parse Server 8 please refer to the [changelog](https://github.com/parse-community/parse-server/blob/alpha/CHANGELOG.md).
|
||||
|
||||
---
|
||||
|
||||
- [Email Verification](#email-verification)
|
||||
- [Database Indexes](#database-indexes)
|
||||
|
||||
---
|
||||
|
||||
## Email Verification
|
||||
|
||||
In order to remove sensitive information (PII) from technical logs, the `Parse.User.username` field has been removed from the email verification process. This means the username will no longer be used and the already existing verification token, that is internal to Parse Server and associated with the user, will be used instead. This makes use of the fact that an expired verification token is not deleted from the database by Parse Server, despite being expired, and can therefore be used to identify a user.
|
||||
|
||||
This change affects how verification emails with expired tokens are handled. When opening a verification link that contains an expired token, the page that the user is redirected to will no longer provide the `username` as a URL query parameter. Instead, the URL query parameter `token` will be provided.
|
||||
|
||||
The request to re-send a verification email changed to sending a `POST` request to the endpoint `/resend_verification_email` with `token` in the body, instead of `username`. If you have customized the HTML pages for email verification either for the `PagesRouter` in `/public/` or the deprecated `PublicAPIRouter` in `/public_html/`, you need to adapt the form request in your custom pages. See the example pages in these aforementioned directories for how the forms must be set up.
|
||||
|
||||
> [!WARNING]
|
||||
> An expired verification token is not automatically deleted from the database by Parse Server even though it has expired. If you have implemented a custom clean-up logic that removes expired tokens, this will break the form request to re-send a verification email as the expired token won't be found and cannot be associated with any user. In that case you'll have to implement your custom process to re-send a verification email.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Parse Server does not keep a history of verification tokens but only stores the most recently generated verification token in the database. Every time Parse Server generates a new verification token, the currently stored token is replaced. If a user opens a link with an expired token, and that token has already been replaced in the database, Parse Server cannot associate the expired token with any user. In this case, another way has to be offered to the user to re-send a verification email. To mitigate this issue, set the Parse Server option `emailVerifyTokenReuseIfValid: true` and set `emailVerifyTokenValidityDuration` to a longer duration, which ensures that the currently stored verification token is not replaced too soon.
|
||||
|
||||
Related pull request:
|
||||
|
||||
- https://github.com/parse-community/parse-server/pull/8488
|
||||
|
||||
## Database Indexes
|
||||
|
||||
As part of the email verification and password reset improvements in Parse Server 8, the queries used for these operations have changed to use tokens instead of username/email fields. To ensure optimal query performance, Parse Server now automatically creates indexes on the following fields during server initialization:
|
||||
|
||||
- `_User._email_verify_token`: used for email verification queries
|
||||
- `_User._perishable_token`: used for password reset queries
|
||||
|
||||
These indexes are created automatically when Parse Server starts, similar to how indexes for `username` and `email` fields are created. No manual intervention is required.
|
||||
|
||||
> [!WARNING]
|
||||
> If you have a large existing user base, the index creation may take some time during the first server startup after upgrading to Parse Server 8. The server logs will indicate when index creation is complete or if any errors occur. If you have any concerns regarding a potential database performance impact during index creation, you could create these indexes manually in a controlled procedure before upgrading Parse Server.
|
||||
|
||||
Related pull request:
|
||||
|
||||
- https://github.com/parse-community/parse-server/pull/9893
|
||||
@@ -15,6 +15,19 @@ Details:
|
||||
- Purpose: official release
|
||||
- Suitable environment: production
|
||||
|
||||
## ⚠️ [Beta Releases][log_beta]
|
||||
|
||||
> ### “Please try out, we’d love your feedback!”
|
||||
|
||||
These are releases that are pretty stable, but may have still some bugs to be fixed before official release.
|
||||
|
||||
Details:
|
||||
- Stability: *pretty stable*
|
||||
- NPM channel: `@beta`
|
||||
- Branch: [beta][branch_beta]
|
||||
- Purpose: feature maturation
|
||||
- Suitable environment: development
|
||||
|
||||
## 🔥 [Alpha Releases][log_alpha]
|
||||
|
||||
> ### “If you are curious to see what's next!”
|
||||
|
||||
+14
-100
@@ -21,13 +21,8 @@
|
||||
- [Good to Know](#good-to-know)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Please Do's](#please-dos)
|
||||
- [TypeScript Tests](#typescript-tests)
|
||||
- [Test against Postgres](#test-against-postgres)
|
||||
- [Postgres with Docker](#postgres-with-docker)
|
||||
- [Performance Testing](#performance-testing)
|
||||
- [Adding Tests](#adding-tests)
|
||||
- [Adding Benchmarks](#adding-benchmarks)
|
||||
- [Benchmark Guidelines](#benchmark-guidelines)
|
||||
- [Breaking Changes](#breaking-changes)
|
||||
- [Deprecation Policy](#deprecation-policy)
|
||||
- [Feature Considerations](#feature-considerations)
|
||||
@@ -44,7 +39,6 @@
|
||||
- [Reverting](#reverting)
|
||||
- [Security Vulnerability](#security-vulnerability)
|
||||
- [Local Testing](#local-testing)
|
||||
- [Environment](#environment)
|
||||
- [Merging](#merging-1)
|
||||
- [Releasing](#releasing)
|
||||
- [General Considerations](#general-considerations)
|
||||
@@ -244,24 +238,11 @@ Once you have babel running in watch mode, you can start making changes to parse
|
||||
* Mocks belong in the `spec/support` folder.
|
||||
* Please consider if any changes to the [docs](http://docs.parseplatform.org) are needed or add additional sections in the case of an enhancement or feature.
|
||||
|
||||
#### TypeScript Tests
|
||||
|
||||
Type tests are located in [/types/tests.ts](/types/tests.ts) and are responsible for ensuring that the type generation for each class is behaving as expected. Types are generated by manually running the script `npm run build:types`. The generated types are `.d.ts` files located in [/types](/types) and must not be manually changed after generation.
|
||||
|
||||
> [!CAUTION]
|
||||
> An exemption are type changes to `src/Options/index.js` which must be manually updated in `types/Options/index.d.ts`, as these types are not generated via a script.
|
||||
|
||||
When developing type definitions you can run `npm run watch:ts` in order to rebuild your changes automatically upon each save. Use `npm run test:types` in order to run types tests against generated `.d.ts` files.
|
||||
|
||||
### Test against Postgres
|
||||
|
||||
If your pull request introduces a change that may affect the storage or retrieval of objects, you may want to make sure it plays nice with Postgres.
|
||||
|
||||
* You'll need to have postgres running on your machine and setup [appropriately](https://github.com/parse-community/parse-server/blob/master/scripts/before_script_postgres.sh) or use [`Docker`](#postgres-with-docker)
|
||||
* Run the tests against the postgres database with:
|
||||
```
|
||||
PARSE_SERVER_TEST_DB=postgres PARSE_SERVER_TEST_DATABASE_URI=postgres://postgres:password@localhost:5432/parse_server_postgres_adapter_test_database npm run testonly
|
||||
```
|
||||
* Run the tests against the postgres database with `PARSE_SERVER_TEST_DB=postgres PARSE_SERVER_TEST_DATABASE_URI=postgres://postgres:password@localhost:5432/parse_server_postgres_adapter_test_database npm run testonly`. You'll need to have postgres running on your machine and setup [appropriately](https://github.com/parse-community/parse-server/blob/master/scripts/before_script_postgres.sh) or use [`Docker`](#run-a-parse-postgres-with-docker).
|
||||
* The Postgres adapter has a special debugger that traces all the sql commands. You can enable it with setting the environment variable `PARSE_SERVER_LOG_LEVEL=debug`
|
||||
* If your feature is intended to only work with MongoDB, you should disable PostgreSQL-specific tests with:
|
||||
|
||||
@@ -277,14 +258,16 @@ If your pull request introduces a change that may affect the storage or retrieva
|
||||
* If your feature is intended to work with MongoDB and PostgreSQL, you can include or exclude tests more granularly with:
|
||||
|
||||
- `it_only_mongodb_version('>=4.4')` // will test with any version of Postgres but only with version >=4.4 of MongoDB; accepts semver notation to specify a version range
|
||||
- `it_exclude_mongodb_version('<4.4')` // will test with any version of Postgres and MongoDB, excluding version <4.4 of MongoDB; accepts semver notation to specify a version range
|
||||
- `it_only_postgres_version('>=13')` // will test with any version of Mongo but only with version >=13 of Postgres; accepts semver notation to specify a version range
|
||||
- `it_exclude_postgres_version('<13')` // will test with any version of Postgres and MongoDB, excluding version <13 of Postgres; accepts semver notation to specify a version range
|
||||
|
||||
#### Postgres with Docker
|
||||
|
||||
[PostGIS images (select one with v2.2 or higher) on docker hub](https://hub.docker.com/r/postgis/postgis) is based off of the official [postgres](https://hub.docker.com/_/postgres) image and will work out-of-the-box (as long as you create a user with the necessary extensions for each of your Parse databases; see below). To launch the compatible Postgres instance, copy and paste the following line into your shell:
|
||||
[PostGIS images (select one with v2.2 or higher) on docker dashboard](https://hub.docker.com/r/postgis/postgis) is based off of the official [postgres](https://registry.hub.docker.com/_/postgres/) image and will work out-of-the-box (as long as you create a user with the necessary extensions for each of your Parse databases; see below). To launch the compatible Postgres instance, copy and paste the following line into your shell:
|
||||
|
||||
```
|
||||
docker run -d --name parse-postgres -p 5432:5432 -e POSTGRES_PASSWORD=password --rm postgis/postgis:17-3.5-alpine && sleep 20 && docker exec -it parse-postgres psql -U postgres -c 'CREATE DATABASE parse_server_postgres_adapter_test_database;' && docker exec -it parse-postgres psql -U postgres -c 'CREATE EXTENSION pgcrypto; CREATE EXTENSION postgis;' -d parse_server_postgres_adapter_test_database && docker exec -it parse-postgres psql -U postgres -c 'CREATE EXTENSION postgis_topology;' -d parse_server_postgres_adapter_test_database
|
||||
docker run -d --name parse-postgres -p 5432:5432 -e POSTGRES_PASSWORD=password --rm postgis/postgis:13-3.2-alpine && sleep 20 && docker exec -it parse-postgres psql -U postgres -c 'CREATE DATABASE parse_server_postgres_adapter_test_database;' && docker exec -it parse-postgres psql -U postgres -c 'CREATE EXTENSION pgcrypto; CREATE EXTENSION postgis;' -d parse_server_postgres_adapter_test_database && docker exec -it parse-postgres psql -U postgres -c 'CREATE EXTENSION postgis_topology;' -d parse_server_postgres_adapter_test_database
|
||||
```
|
||||
To stop the Postgres instance:
|
||||
|
||||
@@ -292,7 +275,7 @@ To stop the Postgres instance:
|
||||
docker stop parse-postgres
|
||||
```
|
||||
|
||||
You can also use the [postgis/postgis:17-3.5-alpine](https://hub.docker.com/r/postgis/postgis) image in a Dockerfile and copy this [script](https://github.com/parse-community/parse-server/blob/master/scripts/before_script_postgres.sh) to the image by adding the following lines:
|
||||
You can also use the [postgis/postgis:13-3.2-alpine](https://hub.docker.com/r/postgis/postgis) image in a Dockerfile and copy this [script](https://github.com/parse-community/parse-server/blob/master/scripts/before_script_postgres.sh) to the image by adding the following lines:
|
||||
|
||||
```
|
||||
#Install additional scripts. These are run in abc order during initial start
|
||||
@@ -302,58 +285,6 @@ 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.
|
||||
|
||||
### Performance Testing
|
||||
|
||||
Parse Server includes an automated performance benchmarking system that runs on every pull request to detect performance regressions and track improvements over time.
|
||||
|
||||
#### Adding Tests
|
||||
|
||||
You should consider adding performance benchmarks if your contribution:
|
||||
|
||||
- **Introduces a performance-critical feature**: Features that will be frequently used in production environments, such as new query operations, authentication methods, or data processing functions.
|
||||
- **Modifies existing critical paths**: Changes to core functionality like object CRUD operations, query execution, user authentication, file operations, or Cloud Code execution.
|
||||
- **Has potential performance impact**: Any change that affects database operations, network requests, data parsing, caching mechanisms, or algorithmic complexity.
|
||||
- **Optimizes performance**: If your PR specifically aims to improve performance, adding benchmarks helps verify the improvement and prevents future regressions.
|
||||
|
||||
#### Adding Benchmarks
|
||||
|
||||
Performance benchmarks are located in [`benchmark/performance.js`](benchmark/performance.js). To add a new benchmark:
|
||||
|
||||
1. **Identify the operation to benchmark**: Determine the specific operation you want to measure (e.g., a new query type, a new API endpoint).
|
||||
|
||||
2. **Create a benchmark function**: Follow the existing patterns in `benchmark/performance.js`:
|
||||
```javascript
|
||||
async function benchmarkNewFeature() {
|
||||
return measureOperation('Feature Name', async () => {
|
||||
// Your operation to benchmark
|
||||
const result = await someOperation();
|
||||
}, ITERATIONS);
|
||||
}
|
||||
```
|
||||
|
||||
3. **Add to benchmark suite**: Register your benchmark in the `runBenchmarks()` function:
|
||||
```javascript
|
||||
console.error('Running New Feature benchmark...');
|
||||
await cleanupDatabase();
|
||||
results.push(await benchmarkNewFeature());
|
||||
```
|
||||
|
||||
4. **Test locally**: Run the benchmarks locally to verify they work:
|
||||
```bash
|
||||
npm run benchmark:quick # Quick test with 10 iterations
|
||||
npm run benchmark # Full test with 10,000 iterations
|
||||
```
|
||||
|
||||
For new features where no baseline exists, the CI will establish new benchmarks that future PRs will be compared against.
|
||||
|
||||
#### Benchmark Guidelines
|
||||
|
||||
- **Keep benchmarks focused**: Each benchmark should test a single, well-defined operation.
|
||||
- **Use realistic data**: Test with data that reflects real-world usage patterns.
|
||||
- **Clean up between runs**: Use `cleanupDatabase()` to ensure consistent test conditions.
|
||||
- **Consider iteration count**: Use fewer iterations for expensive operations (see `ITERATIONS` environment variable).
|
||||
- **Document what you're testing**: Add clear comments explaining what the benchmark measures and why it's important.
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
Breaking changes should be avoided whenever possible. For a breaking change to be accepted, the benefits of the change have to clearly outweigh the costs of developers having to adapt their deployments. If a breaking change is only cosmetic it will likely be rejected and preferred to become obsolete organically during the course of further development, unless it is required as part of a larger change. Breaking changes should follow the [Deprecation Policy](#deprecation-policy).
|
||||
@@ -564,36 +495,19 @@ If the commit reverts a previous commit, use the prefix `revert:`, followed by t
|
||||
|
||||
#### Local Testing
|
||||
|
||||
Fixes for security vulnerabilities are developed in private forks with a closed audience, inaccessible to the public. A current GitHub limitation does not allow to run CI tests on pull requests in private forks. Whether a pull requests fully passes all CI tests can only be determined by publishing the fix as a public pull request and running the CI. This means the fix and implicitly information about the vulnerability are made accessible to the public. This increases the risk that a vulnerability fix is published, but then cannot be merged immediately due to a CI issue. To mitigate that risk, before publishing a vulnerability fix, the following tests needs to be run locally and pass:
|
||||
Fixes for securify vulnerabilities are developed in private forks with a closed audience, inaccessible to the public. A current GitHub limitation does not allow to run CI tests on pull requests in private forks. Whether a pull requests fully passes all CI tests can only be determined by publishing the fix as a public pull request and running the CI. This means the fix and implicitly information about the vulnerabilty are made accessible to the public. This increases the risk that a vulnerability fix is published, but then cannot be merged immediately due to a CI issue. To mitigate that risk, before publishing a vulnerability fix, the following tests needs to be run locally and pass:
|
||||
|
||||
- `npm run test` to test with MongoDB
|
||||
- `npm run test:postgres:testonly` to test with Postgres
|
||||
- `npm run madge:circular` to detect circular dependencies
|
||||
- `npm run lint` to check lint compliance
|
||||
- `npm run definitions` to update the Parse Server options definitions
|
||||
|
||||
> [!CAUTION]
|
||||
> It is essential to run `npm run build` *after* switching to a different branch or making a commit and *before* running any tests. Otherwise the tests may run on the build from a different branch or on a build that does not reflect the most recent commits.
|
||||
|
||||
#### Environment
|
||||
|
||||
A reported vulnerability may have already been fixed since it was reported, either due to a targeted fix or as side-effect of other code changed. To verify that a vulnerability exists, tests need to be run in an environment that uses the latest commit of the development branch of Parse Server.
|
||||
|
||||
> [!NOTE]
|
||||
> Do not use the latest alpha version for testing as it may be behind the latest commit of the development branch.
|
||||
|
||||
Vulnerability test must only be conducted in environments for which the tester can ensure that no unauthorized 3rd party has potentially access to. This is to ensure a vulnerability stays confidential and is not exposed prematurely to the public.
|
||||
|
||||
You must not test a vulnerability using any 3rd party APIs that provide Parse Server as a hosted service (SaaS) as this may expose the vulnerability to an unauthorized 3rd party and the effects of the vulnerability may cause issues on the provider's side.
|
||||
|
||||
> [!CAUTION]
|
||||
> Utilizing a vulnerability in a third-party service, even for testing or development purposes, can result in legal repercussions. You are solely accountable for any damage arising from such actions and agree to indemnify Parse Platform against any liabilities or claims resulting from your actions.
|
||||
- `npm run test` (MongoDB)
|
||||
- `npm run test` (Postgres)
|
||||
- `npm run madge:circular` (circular dependencies)
|
||||
- `npm run lint` (Lint)
|
||||
- `npm run definitions` (Parse Server options definitions)
|
||||
|
||||
#### Merging
|
||||
|
||||
A current GitHub limitation does not allow to customize the commit message when merging pull requests of a private fork that was created to fix a security vulnerability. Our release automation framework demands a specific commit message syntax which therefore cannot be met. This prohibits to follow the process that GitHub suggest, which is to merge a pull request from a private fork directly to a public branch. Instead, after [local testing](#local-testing), a public pull request needs to be created with the code fix copied over from the private pull request.
|
||||
A current GitHub limitation does not allow to customize the commit message when merging pull requests of a private fork that was created to fix a security vulnerabilty. Our release automation framework demands a specific commit message syntax which therefore cannot be met. This prohibits to follow the process that GitHub suggest, which is to merge a pull request from a private fork directly to a public branch. Instead, after [local testing](#local-testing), a public pull request needs to be created with the code fix copied over from the private pull request.
|
||||
|
||||
This creates a risk that a vulnerability is indirectly disclosed by publishing a pull request with the fix, but the fix cannot be merged due to a CI issue. To mitigate that risk, the pull request title and description should be kept marginal or generic, not hinting to a vulnerability or giving any details about the vulnerability, until the pull request has been successfully merged.
|
||||
This creates a risk that a vulnerability is indirectly disclosed by publishing a pull request with the fix, but the fix cannot be merged due to a CI issue. To mitigate that risk, the pull request title and description should be kept marginal or generic, not hiting to a vulnerabilty or giving any details about the vulnerabilty, until the pull request has been successfully merged.
|
||||
|
||||
## Releasing
|
||||
|
||||
|
||||
+12
-15
@@ -2,21 +2,18 @@
|
||||
|
||||
The following is a list of deprecations, according to the [Deprecation Policy](https://github.com/parse-community/parse-server/blob/master/CONTRIBUTING.md#deprecation-policy). After a feature becomes deprecated, and giving developers time to adapt to the change, the deprecated feature will eventually be removed, leading to a breaking change. Developer feedback during the deprecation period may postpone or even revoke the introduction of the breaking change.
|
||||
|
||||
| ID | Change | Issue | Deprecation [ℹ️][i_deprecation] | Planned Removal [ℹ️][i_removal] | Status [ℹ️][i_status] | Notes |
|
||||
|---------|----------------------------------------------------------------------------------------------|----------------------------------------------------------------------|---------------------------------|---------------------------------|-----------------------|-------|
|
||||
| DEPPS1 | Native MongoDB syntax in aggregation pipeline | [#7338](https://github.com/parse-community/parse-server/issues/7338) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - |
|
||||
| DEPPS2 | Config option `directAccess` defaults to `true` | [#6636](https://github.com/parse-community/parse-server/pull/6636) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - |
|
||||
| DEPPS3 | Config option `enforcePrivateUsers` defaults to `true` | [#7319](https://github.com/parse-community/parse-server/pull/7319) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - |
|
||||
| DEPPS4 | Remove convenience method for http request `Parse.Cloud.httpRequest` | [#7589](https://github.com/parse-community/parse-server/pull/7589) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - |
|
||||
| DEPPS5 | Config option `allowClientClassCreation` defaults to `false` | [#7925](https://github.com/parse-community/parse-server/pull/7925) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - |
|
||||
| DEPPS6 | Auth providers disabled by default | [#7953](https://github.com/parse-community/parse-server/pull/7953) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - |
|
||||
| DEPPS7 | Remove file trigger syntax `Parse.Cloud.beforeSaveFile((request) => {})` | [#7966](https://github.com/parse-community/parse-server/pull/7966) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - |
|
||||
| DEPPS8 | Login with expired 3rd party authentication token defaults to `false` | [#7079](https://github.com/parse-community/parse-server/pull/7079) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - |
|
||||
| DEPPS9 | Rename LiveQuery `fields` option to `keys` | [#8389](https://github.com/parse-community/parse-server/issues/8389) | 6.0.0 (2023) | 7.0.0 (2024) | removed | - |
|
||||
| DEPPS10 | Encode `Parse.Object` in Cloud Function and remove option `encodeParseObjectInCloudFunction` | [#8634](https://github.com/parse-community/parse-server/issues/8634) | 6.2.0 (2023) | 9.0.0 (2026) | deprecated | - |
|
||||
| DEPPS11 | Replace `PublicAPIRouter` with `PagesRouter` | [#7625](https://github.com/parse-community/parse-server/issues/7625) | 8.0.0 (2025) | 9.0.0 (2026) | deprecated | - |
|
||||
| DEPPS12 | Database option `allowPublicExplain` will default to `true` | [#7519](https://github.com/parse-community/parse-server/issues/7519) | 8.5.0 (2025) | 9.0.0 (2026) | deprecated | - |
|
||||
| DEPPS20 | Remove config option `allowExpiredAuthDataToken` | | 8.6.0 (2026) | 9.0.0 (2026) | deprecated | - |
|
||||
| ID | Change | Issue | Deprecation [ℹ️][i_deprecation] | Planned Removal [ℹ️][i_removal] | Status [ℹ️][i_status] | Notes |
|
||||
|--------|-------------------------------------------------|----------------------------------------------------------------------|---------------------------------|---------------------------------|-----------------------|-------|
|
||||
| DEPPS1 | Native MongoDB syntax in aggregation pipeline | [#7338](https://github.com/parse-community/parse-server/issues/7338) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - |
|
||||
| DEPPS2 | Config option `directAccess` defaults to `true` | [#6636](https://github.com/parse-community/parse-server/pull/6636) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - |
|
||||
| DEPPS3 | Config option `enforcePrivateUsers` defaults to `true` | [#7319](https://github.com/parse-community/parse-server/pull/7319) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - |
|
||||
| DEPPS4 | Remove convenience method for http request `Parse.Cloud.httpRequest` | [#7589](https://github.com/parse-community/parse-server/pull/7589) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - |
|
||||
| DEPPS5 | Config option `allowClientClassCreation` defaults to `false` | [#7925](https://github.com/parse-community/parse-server/pull/7925) | 5.3.0 (2022) | 7.0.0 (2024) | deprecated | - |
|
||||
| DEPPS6 | Auth providers disabled by default | [#7953](https://github.com/parse-community/parse-server/pull/7953) | 5.3.0 (2022) | 7.0.0 (2024) | deprecated | - |
|
||||
| DEPPS7 | Remove file trigger syntax `Parse.Cloud.beforeSaveFile((request) => {})` | [#7966](https://github.com/parse-community/parse-server/pull/7966) | 5.3.0 (2022) | 7.0.0 (2024) | deprecated | - |
|
||||
| DEPPS8 | Login with expired 3rd party authentication token defaults to `false` | [#7079](https://github.com/parse-community/parse-server/pull/7079) | 5.3.0 (2022) | 7.0.0 (2024) | deprecated | - |
|
||||
| DEPPS9 | Rename LiveQuery `fields` option to `keys` | [#8389](https://github.com/parse-community/parse-server/issues/8389) | 6.0.0 (2023) | 7.0.0 (2024) | deprecated | - |
|
||||
| DEPPS10 | Config option `encodeParseObjectInCloudFunction` defaults to `true` | [#8634](https://github.com/parse-community/parse-server/issues/8634) | 6.2.0 (2023) | 8.0.0 (2025) | deprecated | - |
|
||||
|
||||
[i_deprecation]: ## "The version and date of the deprecation."
|
||||
[i_removal]: ## "The version and date of the planned removal."
|
||||
|
||||
+3
-7
@@ -1,13 +1,9 @@
|
||||
############################################################
|
||||
# Build stage
|
||||
############################################################
|
||||
FROM node:20.19.0-alpine3.20 AS build
|
||||
|
||||
RUN apk --no-cache add \
|
||||
build-base \
|
||||
git \
|
||||
python3
|
||||
FROM node:18-alpine AS build
|
||||
|
||||
RUN apk --no-cache add git
|
||||
WORKDIR /tmp
|
||||
|
||||
# Copy package.json first to benefit from layer caching
|
||||
@@ -28,7 +24,7 @@ RUN npm ci --omit=dev --ignore-scripts \
|
||||
############################################################
|
||||
# Release stage
|
||||
############################################################
|
||||
FROM node:20.19.0-alpine3.20 AS release
|
||||
FROM node:18-alpine AS release
|
||||
|
||||
VOLUME /parse-server/cloud /parse-server/config
|
||||
|
||||
|
||||
@@ -2,17 +2,19 @@
|
||||
|
||||
---
|
||||
|
||||
[](https://github.com/parse-community/parse-server/actions/workflows/ci.yml?query=workflow%3Aci+branch%3Aalpha)
|
||||
[](https://github.com/parse-community/parse-server/actions/workflows/ci.yml?query=workflow%3Aci+branch%3Arelease)
|
||||
[](https://github.com/parse-community/parse-server/actions?query=workflow%3Aci+branch%3Aalpha)
|
||||
[](https://github.com/parse-community/parse-server/actions?query=workflow%3Aci+branch%3Abeta)
|
||||
[](https://github.com/parse-community/parse-server/actions?query=workflow%3Aci+branch%3Arelease)
|
||||
[](https://snyk.io/test/github/parse-community/parse-server)
|
||||
[](https://app.codecov.io/github/parse-community/parse-server/tree/alpha)
|
||||
[](https://codecov.io/github/parse-community/parse-server?branch=alpha)
|
||||
[](https://github.com/parse-community/parse-dashboard/releases)
|
||||
|
||||
[](https://nodejs.org)
|
||||
[](https://www.mongodb.com)
|
||||
[](https://www.postgresql.org)
|
||||
[](https://nodejs.org)
|
||||
[](https://www.mongodb.com)
|
||||
[](https://www.postgresql.org)
|
||||
|
||||
[](https://www.npmjs.com/package/parse-server)
|
||||
[](https://www.npmjs.com/package/parse-server)
|
||||
[](https://www.npmjs.com/package/parse-server)
|
||||
|
||||
[][open-collective-link]
|
||||
@@ -47,7 +49,7 @@ A big *thank you* 🙏 to our [sponsors](#sponsors) and [backers](#backers) who
|
||||
- [PostgreSQL](#postgresql)
|
||||
- [Locally](#locally)
|
||||
- [Docker Container](#docker-container)
|
||||
- [Saving and Querying Objects](#saving-and-querying-objects)
|
||||
- [Saving an Object](#saving-an-object)
|
||||
- [Connect an SDK](#connect-an-sdk)
|
||||
- [Running Parse Server elsewhere](#running-parse-server-elsewhere)
|
||||
- [Sample Application](#sample-application)
|
||||
@@ -98,7 +100,7 @@ A big *thank you* 🙏 to our [sponsors](#sponsors) and [backers](#backers) who
|
||||
|
||||
Parse Server is available in different flavors on different branches:
|
||||
|
||||
- The main branches are [release][log_release] and [alpha][log_alpha]. See the [changelog overview](CHANGELOG.md) for details.
|
||||
- The main branches are [release][log_release], [beta][log_beta] and [alpha][log_alpha]. See the [changelog overview](CHANGELOG.md) for details.
|
||||
- The long-term-support (LTS) branches are named `release-<version>.x.x`, for example `release-5.x.x`. LTS branches do not have pre-release branches.
|
||||
|
||||
## Long Term Support
|
||||
@@ -125,35 +127,36 @@ Before you start make sure you have installed:
|
||||
|
||||
Parse Server is continuously tested with the most recent releases of Node.js to ensure compatibility. We follow the [Node.js Long Term Support plan](https://github.com/nodejs/Release) and only test against versions that are officially supported and have not reached their end-of-life date.
|
||||
|
||||
| Version | Minimum Version | End-of-Life | Parse Server Support |
|
||||
|------------|-----------------|-------------|----------------------|
|
||||
| Node.js 18 | 18.20.4 | April 2025 | <= 8.x (2025) |
|
||||
| Node.js 20 | 20.18.0 | April 2026 | <= 9.x (2026) |
|
||||
| Node.js 22 | 22.12.0 | April 2027 | <= 10.x (2027) |
|
||||
| Node.js 24 | 24.11.0 | April 2028 | <= 11.x (2028) |
|
||||
| Version | Latest Version | End-of-Life | Compatible |
|
||||
|------------|----------------|-------------|------------|
|
||||
| Node.js 14 | 14.19.1 | April 2023 | ✅ Yes |
|
||||
| Node.js 16 | 16.14.2 | April 2024 | ✅ Yes |
|
||||
| Node.js 18 | 18.12.1 | April 2025 | ✅ Yes |
|
||||
| Node.js 19 | 19.3.0 | June 2023 | ✅ Yes |
|
||||
|
||||
#### MongoDB
|
||||
|
||||
Parse Server is continuously tested with the most recent releases of MongoDB to ensure compatibility. We follow the [MongoDB support schedule](https://www.mongodb.com/support-policy) and [MongoDB lifecycle schedule](https://www.mongodb.com/support-policy/lifecycles) and only test against versions that are officially supported and have not reached their end-of-life date. MongoDB "rapid releases" are ignored as these are considered pre-releases of the next major version.
|
||||
Parse Server is continuously tested with the most recent releases of MongoDB to ensure compatibility. We follow the [MongoDB support schedule](https://www.mongodb.com/support-policy) and [MongoDB lifecycle schedule](https://www.mongodb.com/support-policy/lifecycles) and only test against versions that are officially supported and have not reached their end-of-life date. We consider the end-of-life date of a MongoDB "rapid release" to be the same as its major version release.
|
||||
|
||||
| Version | Minimum Version | End-of-Life | Parse Server Support |
|
||||
|-----------|-----------------|-------------|----------------------|
|
||||
| MongoDB 6 | 6.0.19 | July 2025 | <= 8.x (2025) |
|
||||
| MongoDB 7 | 7.0.16 | August 2026 | <= 9.x (2026) |
|
||||
| MongoDB 8 | 8.0.4 | TDB | <= 10.x (2027) |
|
||||
| Version | Latest Version | End-of-Life | Compatible |
|
||||
|-------------|----------------|---------------|------------|
|
||||
| MongoDB 4.0 | 4.0.28 | April 2022 | ✅ Yes |
|
||||
| MongoDB 4.2 | 4.2.19 | April 2023 | ✅ Yes |
|
||||
| MongoDB 4.4 | 4.4.13 | February 2024 | ✅ Yes |
|
||||
| MongoDB 5 | 5.3.2 | October 2024 | ✅ Yes |
|
||||
| MongoDB 6 | 6.0.2 | July 2025 | ✅ Yes |
|
||||
|
||||
#### PostgreSQL
|
||||
|
||||
Parse Server is continuously tested with the most recent releases of PostgreSQL and PostGIS to ensure compatibility, using [PostGIS docker images](https://registry.hub.docker.com/r/postgis/postgis/tags?page=1&ordering=last_updated). We follow the [PostgreSQL support schedule](https://www.postgresql.org/support/versioning) and [PostGIS support schedule](https://www.postgis.net/eol_policy/) and only test against versions that are officially supported and have not reached their end-of-life date. Due to the extensive PostgreSQL support duration of 5 years, Parse Server drops support about 2 years before the official end-of-life date.
|
||||
|
||||
| Version | PostGIS Version | End-of-Life | Parse Server Support |
|
||||
|-------------|-------------------------|---------------|----------------------|
|
||||
| Postgres 13 | 3.1, 3.2, 3.3, 3.4, 3.5 | November 2025 | <= 6.x (2023) |
|
||||
| Postgres 14 | 3.5 | November 2026 | <= 7.x (2024) |
|
||||
| Postgres 15 | 3.3, 3.4, 3.5 | November 2027 | <= 8.x (2025) |
|
||||
| Postgres 16 | 3.5 | November 2028 | <= 9.x (2026) |
|
||||
| Postgres 17 | 3.5 | November 2029 | <= 10.x (2027) |
|
||||
| Postgres 18 | 3.6 | November 2030 | <= 11.x (2028) |
|
||||
| Version | PostGIS Version | End-of-Life | Parse Server Support | Compatible |
|
||||
|-------------|--------------------|---------------|----------------------|------------|
|
||||
| Postgres 11 | 3.0, 3.1, 3.2, 3.3 | November 2023 | <= 5.x (2022) | ✅ Yes |
|
||||
| Postgres 12 | 3.3 | November 2024 | <= 5.x (2022) | ✅ Yes |
|
||||
| Postgres 13 | 3.3 | November 2025 | <= 6.x (2023) | ✅ Yes |
|
||||
| Postgres 14 | 3.3 | November 2026 | <= 7.x (2024) | ✅ Yes |
|
||||
| Postgres 15 | 3.3 | November 2027 | <= 8.x (2025) | ✅ Yes |
|
||||
|
||||
### Locally
|
||||
|
||||
@@ -188,9 +191,70 @@ That's it! You are now running a standalone version of Parse Server on your mach
|
||||
|
||||
**Using a remote MongoDB?** Pass the `--databaseURI DATABASE_URI` parameter when starting `parse-server`. Learn more about configuring Parse Server [here](#configuration). For a full list of available options, run `parse-server --help`.
|
||||
|
||||
### Saving and Querying Objects
|
||||
### Saving an Object
|
||||
|
||||
Now that you're running Parse Server, it is time to save your first object. The easiest way is to use the [REST API](http://docs.parseplatform.org/rest/guide), but you can easily do the same using any of the [Parse SDKs](http://parseplatform.org/#sdks). To learn more check out the [documentation](http://docs.parseplatform.org).
|
||||
Now that you're running Parse Server, it is time to save your first object. We'll use the [REST API](http://docs.parseplatform.org/rest/guide), but you can easily do the same using any of the [Parse SDKs](http://parseplatform.org/#sdks). Run the following:
|
||||
|
||||
```bash
|
||||
$ curl -X POST \
|
||||
-H "X-Parse-Application-Id: APPLICATION_ID" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"score":1337,"playerName":"Sean Plott","cheatMode":false}' \
|
||||
http://localhost:1337/parse/classes/GameScore
|
||||
```
|
||||
|
||||
You should get a response similar to this:
|
||||
|
||||
```js
|
||||
{
|
||||
"objectId": "2ntvSpRGIK",
|
||||
"createdAt": "2016-03-11T23:51:48.050Z"
|
||||
}
|
||||
```
|
||||
|
||||
You can now retrieve this object directly (make sure to replace `2ntvSpRGIK` with the actual `objectId` you received when the object was created):
|
||||
|
||||
```bash
|
||||
$ curl -X GET \
|
||||
-H "X-Parse-Application-Id: APPLICATION_ID" \
|
||||
http://localhost:1337/parse/classes/GameScore/2ntvSpRGIK
|
||||
```
|
||||
```json
|
||||
// Response
|
||||
{
|
||||
"objectId": "2ntvSpRGIK",
|
||||
"score": 1337,
|
||||
"playerName": "Sean Plott",
|
||||
"cheatMode": false,
|
||||
"updatedAt": "2016-03-11T23:51:48.050Z",
|
||||
"createdAt": "2016-03-11T23:51:48.050Z"
|
||||
}
|
||||
```
|
||||
|
||||
Keeping tracks of individual object ids is not ideal, however. In most cases you will want to run a query over the collection, like so:
|
||||
|
||||
```bash
|
||||
$ curl -X GET \
|
||||
-H "X-Parse-Application-Id: APPLICATION_ID" \
|
||||
http://localhost:1337/parse/classes/GameScore
|
||||
```
|
||||
```json
|
||||
// The response will provide all the matching objects within the `results` array:
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"objectId": "2ntvSpRGIK",
|
||||
"score": 1337,
|
||||
"playerName": "Sean Plott",
|
||||
"cheatMode": false,
|
||||
"updatedAt": "2016-03-11T23:51:48.050Z",
|
||||
"createdAt": "2016-03-11T23:51:48.050Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
To learn more about using saving and querying objects on Parse Server, check out the [Parse documentation](http://docs.parseplatform.org).
|
||||
|
||||
### Connect an SDK
|
||||
|
||||
@@ -214,7 +278,6 @@ We have provided a basic [Node.js application](https://github.com/parse-communit
|
||||
* [Back4app](https://www.back4app.com/docs/get-started/welcome)
|
||||
* [Glitch](https://glitch.com/edit/#!/parse-server)
|
||||
* [Flynn](https://flynn.io/blog/parse-apps-on-flynn)
|
||||
* [Elestio](https://elest.io/open-source/parse)
|
||||
|
||||
### Parse Server + Express
|
||||
|
||||
@@ -245,7 +308,7 @@ app.listen(1337, function() {
|
||||
});
|
||||
```
|
||||
|
||||
For a full list of available options, run `parse-server --help` or take a look at [Parse Server Configurations][server-options].
|
||||
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).
|
||||
|
||||
## Parse Server Health
|
||||
|
||||
@@ -272,7 +335,7 @@ The response looks like this:
|
||||
|
||||
Parse Server can be configured using the following options. You may pass these as parameters when running a standalone `parse-server`, or by loading a configuration file in JSON format using `parse-server path/to/configuration.json`. If you're using Parse Server on Express, you may also pass these to the `ParseServer` object as options.
|
||||
|
||||
For the full list of available options, run `parse-server --help` or take a look at [Parse Server Configurations][server-options].
|
||||
For the full list of available options, run `parse-server --help` or take a look at [Parse Server Configurations](http://parseplatform.org/parse-server/api/master/ParseServerOptions.html).
|
||||
|
||||
## Basic Options
|
||||
|
||||
@@ -306,7 +369,7 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo
|
||||
|
||||
## Email Verification and Password Reset
|
||||
|
||||
Verifying user email addresses and enabling password reset via email requires an email adapter. There are many email adapters provided and maintained by the community. The following is an example configuration with an example email adapter. See the [Parse Server Options][server-options] for more details and a full list of available options.
|
||||
Verifying user email addresses and enabling password reset via email requires an email adapter. There are many email adapters provided and maintained by the community. The following is an example configuration with an example email adapter. See the [Parse Server Options](https://parseplatform.org/parse-server/api/master/ParseServerOptions.html) for more details and a full list of available options.
|
||||
|
||||
```js
|
||||
const server = ParseServer({
|
||||
@@ -346,7 +409,7 @@ Email adapters contributed by the community:
|
||||
|
||||
## Password and Account Policy
|
||||
|
||||
Set a password and account policy that meets your security requirements. The following is an example configuration. See the [Parse Server Options][server-options] for more details and a full list of available options.
|
||||
Set a password and account policy that meets your security requirements. The following is an example configuration. See the [Parse Server Options](https://parseplatform.org/parse-server/api/master/ParseServerOptions.html) for more details and a full list of available options.
|
||||
|
||||
```js
|
||||
const server = ParseServer({
|
||||
@@ -376,6 +439,8 @@ const server = ParseServer({
|
||||
|
||||
## Custom Routes
|
||||
|
||||
**Caution, this is an experimental feature that may not be appropriate for production.**
|
||||
|
||||
Custom routes allow to build user flows with webpages, similar to the existing password reset and email verification features. Custom routes are defined with the `pages` option in the Parse Server configuration:
|
||||
|
||||
### Example
|
||||
@@ -385,7 +450,7 @@ const api = new ParseServer({
|
||||
...otherOptions,
|
||||
|
||||
pages: {
|
||||
enableRouter: true,
|
||||
enableRouter: true, // Enables the experimental feature; required for custom routes
|
||||
customRoutes: [{
|
||||
method: 'GET',
|
||||
path: 'custom_route',
|
||||
@@ -422,7 +487,7 @@ The following paths are already used by Parse Server's built-in features and are
|
||||
| Parameter | Optional | Type | Default value | Example values | Environment variable | Description |
|
||||
|------------------------------|----------|-----------------|---------------|-----------------------|------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `pages` | yes | `Object` | `undefined` | - | `PARSE_SERVER_PAGES` | The options for pages such as password reset and email verification. |
|
||||
| `pages.enableRouter` | yes | `Boolean` | `false` | - | `PARSE_SERVER_PAGES_ENABLE_ROUTER` | Is `true` if the pages router should be enabled; this is required for any of the pages options to take effect. |
|
||||
| `pages.enableRouter` | yes | `Boolean` | `false` | - | `PARSE_SERVER_PAGES_ENABLE_ROUTER` | Is `true` if the pages router should be enabled; this is required for any of the pages options to take effect. **Caution, this is an experimental feature that may not be appropriate for production.** |
|
||||
| `pages.customRoutes` | yes | `Array` | `[]` | - | `PARSE_SERVER_PAGES_CUSTOM_ROUTES` | The custom routes. The routes are added in the order they are defined here, which has to be considered since requests traverse routes in an ordered manner. Custom routes are traversed after build-in routes such as password reset and email verification. |
|
||||
| `pages.customRoutes.method` | | `String` | - | `GET`, `POST` | - | The HTTP method of the custom route. |
|
||||
| `pages.customRoutes.path` | | `String` | - | `custom_page` | - | The path of the custom route. Note that the same path can used if the `method` is different, for example a path `custom_page` can have two routes, a `GET` and `POST` route, which will be invoked depending on the HTTP request method. |
|
||||
@@ -545,6 +610,8 @@ Assuming the script above is named, `parse_idempotency_delete_expired_records.sh
|
||||
|
||||
### Pages
|
||||
|
||||
**Caution, this is an experimental feature that may not be appropriate for production.**
|
||||
|
||||
Custom pages as well as feature pages (e.g. password reset, email verification) can be localized with the `pages` option in the Parse Server configuration:
|
||||
|
||||
```js
|
||||
@@ -552,7 +619,7 @@ const api = new ParseServer({
|
||||
...otherOptions,
|
||||
|
||||
pages: {
|
||||
enableRouter: true,
|
||||
enableRouter: true, // Enables the experimental feature; required for localization
|
||||
enableLocalization: true,
|
||||
}
|
||||
}
|
||||
@@ -600,7 +667,7 @@ const api = new ParseServer({
|
||||
...otherOptions,
|
||||
|
||||
pages: {
|
||||
enableRouter: true,
|
||||
enableRouter: true, // Enables the experimental feature; required for localization
|
||||
enableLocalization: true,
|
||||
customUrls: {
|
||||
passwordReset: 'https://example.com/page.html'
|
||||
@@ -657,7 +724,7 @@ const api = new ParseServer({
|
||||
...otherOptions,
|
||||
|
||||
pages: {
|
||||
enableRouter: true,
|
||||
enableRouter: true, // Enables the experimental feature; required for localization
|
||||
enableLocalization: true,
|
||||
localizationJsonPath: './private/localization.json',
|
||||
localizationFallbackLocale: 'en'
|
||||
@@ -682,7 +749,7 @@ const api = new ParseServer({
|
||||
...otherOptions,
|
||||
|
||||
pages: {
|
||||
enableRouter: true,
|
||||
enableRouter: true, // Enables the experimental feature; required for localization
|
||||
placeholders: {
|
||||
exampleKey: 'exampleValue'
|
||||
}
|
||||
@@ -696,7 +763,7 @@ const api = new ParseServer({
|
||||
...otherOptions,
|
||||
|
||||
pages: {
|
||||
enableRouter: true,
|
||||
enableRouter: true, // Enables the experimental feature; required for localization
|
||||
placeholders: async (params) => {
|
||||
const value = await doSomething(params.locale);
|
||||
return {
|
||||
@@ -716,7 +783,7 @@ The following parameter and placeholder keys are reserved because they are used
|
||||
| Parameter | Optional | Type | Default value | Example values | Environment variable | Description |
|
||||
|-------------------------------------------------|----------|---------------------------------------|----------------------------------------|------------------------------------------------------|-----------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `pages` | yes | `Object` | `undefined` | - | `PARSE_SERVER_PAGES` | The options for pages such as password reset and email verification. |
|
||||
| `pages.enableRouter` | yes | `Boolean` | `false` | - | `PARSE_SERVER_PAGES_ENABLE_ROUTER` | Is `true` if the pages router should be enabled; this is required for any of the pages options to take effect. |
|
||||
| `pages.enableRouter` | yes | `Boolean` | `false` | - | `PARSE_SERVER_PAGES_ENABLE_ROUTER` | Is `true` if the pages router should be enabled; this is required for any of the pages options to take effect. **Caution, this is an experimental feature that may not be appropriate for production.** |
|
||||
| `pages.enableLocalization` | yes | `Boolean` | `false` | - | `PARSE_SERVER_PAGES_ENABLE_LOCALIZATION` | Is true if pages should be localized; this has no effect on custom page redirects. |
|
||||
| `pages.localizationJsonPath` | yes | `String` | `undefined` | `./private/translations.json` | `PARSE_SERVER_PAGES_LOCALIZATION_JSON_PATH` | The path to the JSON file for localization; the translations will be used to fill template placeholders according to the locale. |
|
||||
| `pages.localizationFallbackLocale` | yes | `String` | `en` | `en`, `en-GB`, `default` | `PARSE_SERVER_PAGES_LOCALIZATION_FALLBACK_LOCALE` | The fallback locale for localization if no matching translation is provided for the given locale. This is only relevant when providing translation resources via JSON file. |
|
||||
@@ -1142,4 +1209,3 @@ Support us with a monthly donation and help us continue our activities. [Become
|
||||
[log_release]: https://github.com/parse-community/parse-server/blob/release/changelogs/CHANGELOG_release.md
|
||||
[log_beta]: https://github.com/parse-community/parse-server/blob/beta/changelogs/CHANGELOG_beta.md
|
||||
[log_alpha]: https://github.com/parse-community/parse-server/blob/alpha/changelogs/CHANGELOG_alpha.md
|
||||
[server-options] http://parseplatform.org/parse-server/api/release/ParseServerOptions.html
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
/**
|
||||
* MongoDB Latency Wrapper
|
||||
*
|
||||
* Utility to inject artificial latency into MongoDB operations for performance testing.
|
||||
* This wrapper temporarily wraps MongoDB Collection methods to add delays before
|
||||
* database operations execute.
|
||||
*
|
||||
* Usage:
|
||||
* const { wrapMongoDBWithLatency } = require('./MongoLatencyWrapper');
|
||||
*
|
||||
* // Before initializing Parse Server
|
||||
* const unwrap = wrapMongoDBWithLatency(10); // 10ms delay
|
||||
*
|
||||
* // ... run benchmarks ...
|
||||
*
|
||||
* // Cleanup when done
|
||||
* unwrap();
|
||||
*/
|
||||
|
||||
const { Collection } = require('mongodb');
|
||||
|
||||
// Store original methods for restoration
|
||||
const originalMethods = new Map();
|
||||
|
||||
/**
|
||||
* Wrap a Collection method to add artificial latency
|
||||
* @param {string} methodName - Name of the method to wrap
|
||||
* @param {number} latencyMs - Delay in milliseconds
|
||||
*/
|
||||
function wrapMethod(methodName, latencyMs) {
|
||||
if (!originalMethods.has(methodName)) {
|
||||
originalMethods.set(methodName, Collection.prototype[methodName]);
|
||||
}
|
||||
|
||||
const originalMethod = originalMethods.get(methodName);
|
||||
|
||||
Collection.prototype[methodName] = function (...args) {
|
||||
// For methods that return cursors (like find, aggregate), we need to delay the execution
|
||||
// but still return a cursor-like object
|
||||
const result = originalMethod.apply(this, args);
|
||||
|
||||
// Check if result has cursor methods (toArray, forEach, etc.)
|
||||
if (result && typeof result.toArray === 'function') {
|
||||
// Wrap cursor methods that actually execute the query
|
||||
const originalToArray = result.toArray.bind(result);
|
||||
result.toArray = function() {
|
||||
// Wait for the original promise to settle, then delay the result
|
||||
return originalToArray().then(
|
||||
value => new Promise(resolve => setTimeout(() => resolve(value), latencyMs)),
|
||||
error => new Promise((_, reject) => setTimeout(() => reject(error), latencyMs))
|
||||
);
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
// For promise-returning methods, wrap the promise with delay
|
||||
if (result && typeof result.then === 'function') {
|
||||
// Wait for the original promise to settle, then delay the result
|
||||
return result.then(
|
||||
value => new Promise(resolve => setTimeout(() => resolve(value), latencyMs)),
|
||||
error => new Promise((_, reject) => setTimeout(() => reject(error), latencyMs))
|
||||
);
|
||||
}
|
||||
|
||||
// For synchronous methods, just add delay
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(result);
|
||||
}, latencyMs);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap MongoDB Collection methods with artificial latency
|
||||
* @param {number} latencyMs - Delay in milliseconds to inject before each operation
|
||||
* @returns {Function} unwrap - Function to restore original methods
|
||||
*/
|
||||
function wrapMongoDBWithLatency(latencyMs) {
|
||||
if (typeof latencyMs !== 'number' || latencyMs < 0) {
|
||||
throw new Error('latencyMs must be a non-negative number');
|
||||
}
|
||||
|
||||
if (latencyMs === 0) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Latency is 0ms, skipping MongoDB wrapping');
|
||||
return () => {}; // No-op unwrap function
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Wrapping MongoDB operations with ${latencyMs}ms artificial latency`);
|
||||
|
||||
// List of MongoDB Collection methods to wrap
|
||||
const methodsToWrap = [
|
||||
'find',
|
||||
'findOne',
|
||||
'countDocuments',
|
||||
'estimatedDocumentCount',
|
||||
'distinct',
|
||||
'aggregate',
|
||||
'insertOne',
|
||||
'insertMany',
|
||||
'updateOne',
|
||||
'updateMany',
|
||||
'replaceOne',
|
||||
'deleteOne',
|
||||
'deleteMany',
|
||||
'findOneAndUpdate',
|
||||
'findOneAndReplace',
|
||||
'findOneAndDelete',
|
||||
'createIndex',
|
||||
'createIndexes',
|
||||
'dropIndex',
|
||||
'dropIndexes',
|
||||
'drop',
|
||||
];
|
||||
|
||||
methodsToWrap.forEach(methodName => {
|
||||
wrapMethod(methodName, latencyMs);
|
||||
});
|
||||
|
||||
// Return unwrap function to restore original methods
|
||||
return function unwrap() {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Removing MongoDB latency wrapper, restoring original methods');
|
||||
|
||||
originalMethods.forEach((originalMethod, methodName) => {
|
||||
Collection.prototype[methodName] = originalMethod;
|
||||
});
|
||||
|
||||
originalMethods.clear();
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
wrapMongoDBWithLatency,
|
||||
};
|
||||
@@ -1,631 +0,0 @@
|
||||
/**
|
||||
* Performance Benchmark Suite for Parse Server
|
||||
*
|
||||
* This suite measures the performance of critical Parse Server operations
|
||||
* using the Node.js Performance API. Results are output in a format
|
||||
* compatible with github-action-benchmark.
|
||||
*
|
||||
* Run with: npm run benchmark
|
||||
*/
|
||||
|
||||
const core = require('@actions/core');
|
||||
const Parse = require('parse/node');
|
||||
const { performance } = require('node:perf_hooks');
|
||||
const { MongoClient } = require('mongodb');
|
||||
const { wrapMongoDBWithLatency } = require('./MongoLatencyWrapper');
|
||||
|
||||
// Configuration
|
||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/parse_benchmark_test';
|
||||
const SERVER_URL = 'http://localhost:1337/parse';
|
||||
const APP_ID = 'benchmark-app-id';
|
||||
const MASTER_KEY = 'benchmark-master-key';
|
||||
const ITERATIONS = process.env.BENCHMARK_ITERATIONS ? parseInt(process.env.BENCHMARK_ITERATIONS, 10) : undefined;
|
||||
const LOG_ITERATIONS = false;
|
||||
|
||||
// Parse Server instance
|
||||
let parseServer;
|
||||
let mongoClient;
|
||||
|
||||
// Logging helpers
|
||||
const logInfo = message => core.info(message);
|
||||
const logError = message => core.error(message);
|
||||
|
||||
/**
|
||||
* Initialize Parse Server for benchmarking
|
||||
*/
|
||||
async function initializeParseServer() {
|
||||
const express = require('express');
|
||||
const { default: ParseServer } = require('../lib/index.js');
|
||||
|
||||
const app = express();
|
||||
|
||||
parseServer = new ParseServer({
|
||||
databaseURI: MONGODB_URI,
|
||||
appId: APP_ID,
|
||||
masterKey: MASTER_KEY,
|
||||
serverURL: SERVER_URL,
|
||||
silent: true,
|
||||
allowClientClassCreation: true,
|
||||
logLevel: 'error', // Minimal logging for performance
|
||||
verbose: false,
|
||||
});
|
||||
|
||||
app.use('/parse', parseServer.app);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = app.listen(1337, (err) => {
|
||||
if (err) {
|
||||
reject(new Error(`Failed to start server: ${err.message}`));
|
||||
return;
|
||||
}
|
||||
Parse.initialize(APP_ID);
|
||||
Parse.masterKey = MASTER_KEY;
|
||||
Parse.serverURL = SERVER_URL;
|
||||
resolve(server);
|
||||
});
|
||||
|
||||
server.on('error', (err) => {
|
||||
reject(new Error(`Server error: ${err.message}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up database between benchmarks
|
||||
*/
|
||||
async function cleanupDatabase() {
|
||||
try {
|
||||
if (!mongoClient) {
|
||||
mongoClient = await MongoClient.connect(MONGODB_URI);
|
||||
}
|
||||
const db = mongoClient.db();
|
||||
const collections = await db.listCollections().toArray();
|
||||
|
||||
for (const collection of collections) {
|
||||
if (!collection.name.startsWith('system.')) {
|
||||
await db.collection(collection.name).deleteMany({});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to cleanup database: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset Parse SDK to use the default server
|
||||
*/
|
||||
function resetParseServer() {
|
||||
Parse.serverURL = SERVER_URL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Measure average time for an async operation over multiple iterations.
|
||||
* @param {Object} options Measurement options.
|
||||
* @param {string} options.name Name of the operation being measured.
|
||||
* @param {Function} options.operation Async function to measure.
|
||||
* @param {number} options.iterations Number of iterations to run; choose a value that is high
|
||||
* enough to create reliable benchmark metrics with low variance but low enough to keep test
|
||||
* duration reasonable around <=10 seconds.
|
||||
* @param {boolean} [options.skipWarmup=false] Skip warmup phase.
|
||||
* @param {number} [options.dbLatency] Artificial DB latency in milliseconds to apply during
|
||||
* this benchmark.
|
||||
*/
|
||||
async function measureOperation({ name, operation, iterations, skipWarmup = false, dbLatency }) {
|
||||
// Override iterations if global ITERATIONS is set
|
||||
iterations = ITERATIONS || iterations;
|
||||
|
||||
// Determine warmup count (20% of iterations)
|
||||
const warmupCount = skipWarmup ? 0 : Math.floor(iterations * 0.2);
|
||||
const times = [];
|
||||
|
||||
// Apply artificial latency if specified
|
||||
let unwrapLatency = null;
|
||||
if (dbLatency !== undefined && dbLatency > 0) {
|
||||
logInfo(`Applying ${dbLatency}ms artificial DB latency for this benchmark`);
|
||||
unwrapLatency = wrapMongoDBWithLatency(dbLatency);
|
||||
}
|
||||
|
||||
try {
|
||||
if (warmupCount > 0) {
|
||||
logInfo(`Starting warmup phase of ${warmupCount} iterations...`);
|
||||
const warmupStart = performance.now();
|
||||
for (let i = 0; i < warmupCount; i++) {
|
||||
await operation();
|
||||
}
|
||||
logInfo(`Warmup took: ${(performance.now() - warmupStart).toFixed(2)}ms`);
|
||||
}
|
||||
|
||||
// Measurement phase
|
||||
logInfo(`Starting measurement phase of ${iterations} iterations...`);
|
||||
const progressInterval = Math.ceil(iterations / 10); // Log every 10%
|
||||
const measurementStart = performance.now();
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const start = performance.now();
|
||||
await operation();
|
||||
const end = performance.now();
|
||||
const duration = end - start;
|
||||
times.push(duration);
|
||||
|
||||
// Log progress every 10% or individual iterations if LOG_ITERATIONS is enabled
|
||||
if (LOG_ITERATIONS) {
|
||||
logInfo(`Iteration ${i + 1}: ${duration.toFixed(2)}ms`);
|
||||
} else if ((i + 1) % progressInterval === 0 || i + 1 === iterations) {
|
||||
const progress = Math.round(((i + 1) / iterations) * 100);
|
||||
logInfo(`Progress: ${progress}%`);
|
||||
}
|
||||
}
|
||||
|
||||
logInfo(`Measurement took: ${(performance.now() - measurementStart).toFixed(2)}ms`);
|
||||
|
||||
// Sort times for percentile calculations
|
||||
times.sort((a, b) => a - b);
|
||||
|
||||
// Filter outliers using Interquartile Range (IQR) method
|
||||
const q1Index = Math.floor(times.length * 0.25);
|
||||
const q3Index = Math.floor(times.length * 0.75);
|
||||
const q1 = times[q1Index];
|
||||
const q3 = times[q3Index];
|
||||
const iqr = q3 - q1;
|
||||
const lowerBound = q1 - 1.5 * iqr;
|
||||
const upperBound = q3 + 1.5 * iqr;
|
||||
|
||||
const filtered = times.filter(t => t >= lowerBound && t <= upperBound);
|
||||
|
||||
// Calculate statistics on filtered data
|
||||
const median = filtered[Math.floor(filtered.length * 0.5)];
|
||||
const p95 = filtered[Math.floor(filtered.length * 0.95)];
|
||||
const p99 = filtered[Math.floor(filtered.length * 0.99)];
|
||||
const min = filtered[0];
|
||||
const max = filtered[filtered.length - 1];
|
||||
|
||||
return {
|
||||
name,
|
||||
value: median, // Use median (p50) as primary metric for stability in CI
|
||||
unit: 'ms',
|
||||
range: `${min.toFixed(2)} - ${max.toFixed(2)}`,
|
||||
extra: `p95: ${p95.toFixed(2)}ms, p99: ${p99.toFixed(2)}ms, n=${filtered.length}/${times.length}`,
|
||||
};
|
||||
} finally {
|
||||
// Remove latency wrapper if it was applied
|
||||
if (unwrapLatency) {
|
||||
unwrapLatency();
|
||||
logInfo('Removed artificial DB latency');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Benchmark: Object Create
|
||||
*/
|
||||
async function benchmarkObjectCreate(name) {
|
||||
let counter = 0;
|
||||
|
||||
return measureOperation({
|
||||
name,
|
||||
iterations: 1_000,
|
||||
operation: async () => {
|
||||
const TestObject = Parse.Object.extend('BenchmarkTest');
|
||||
const obj = new TestObject();
|
||||
obj.set('testField', `test-value-${counter++}`);
|
||||
obj.set('number', counter);
|
||||
obj.set('boolean', true);
|
||||
await obj.save();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Benchmark: Object Read (by ID)
|
||||
*/
|
||||
async function benchmarkObjectRead(name) {
|
||||
// Setup: Create test objects
|
||||
const TestObject = Parse.Object.extend('BenchmarkTest');
|
||||
const objects = [];
|
||||
|
||||
for (let i = 0; i < 1_000; i++) {
|
||||
const obj = new TestObject();
|
||||
obj.set('testField', `read-test-${i}`);
|
||||
objects.push(obj);
|
||||
}
|
||||
|
||||
await Parse.Object.saveAll(objects);
|
||||
|
||||
let counter = 0;
|
||||
|
||||
return measureOperation({
|
||||
name,
|
||||
iterations: 1_000,
|
||||
operation: async () => {
|
||||
const query = new Parse.Query('BenchmarkTest');
|
||||
await query.get(objects[counter++ % objects.length].id);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Benchmark: Object Update
|
||||
*/
|
||||
async function benchmarkObjectUpdate(name) {
|
||||
// Setup: Create test objects
|
||||
const TestObject = Parse.Object.extend('BenchmarkTest');
|
||||
const objects = [];
|
||||
|
||||
for (let i = 0; i < 1_000; i++) {
|
||||
const obj = new TestObject();
|
||||
obj.set('testField', `update-test-${i}`);
|
||||
obj.set('counter', 0);
|
||||
objects.push(obj);
|
||||
}
|
||||
|
||||
await Parse.Object.saveAll(objects);
|
||||
|
||||
let counter = 0;
|
||||
|
||||
return measureOperation({
|
||||
name,
|
||||
iterations: 1_000,
|
||||
operation: async () => {
|
||||
const obj = objects[counter++ % objects.length];
|
||||
obj.increment('counter');
|
||||
obj.set('lastUpdated', new Date());
|
||||
await obj.save();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Benchmark: Simple Query
|
||||
*/
|
||||
async function benchmarkSimpleQuery(name) {
|
||||
// Setup: Create test data
|
||||
const TestObject = Parse.Object.extend('BenchmarkTest');
|
||||
const objects = [];
|
||||
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const obj = new TestObject();
|
||||
obj.set('category', i % 10);
|
||||
obj.set('value', i);
|
||||
objects.push(obj);
|
||||
}
|
||||
|
||||
await Parse.Object.saveAll(objects);
|
||||
|
||||
let counter = 0;
|
||||
|
||||
return measureOperation({
|
||||
name,
|
||||
iterations: 1_000,
|
||||
operation: async () => {
|
||||
const query = new Parse.Query('BenchmarkTest');
|
||||
query.equalTo('category', counter++ % 10);
|
||||
await query.find();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Benchmark: Batch Save (saveAll)
|
||||
*/
|
||||
async function benchmarkBatchSave(name) {
|
||||
const BATCH_SIZE = 10;
|
||||
|
||||
return measureOperation({
|
||||
name,
|
||||
iterations: 1_000,
|
||||
operation: async () => {
|
||||
const TestObject = Parse.Object.extend('BenchmarkTest');
|
||||
const objects = [];
|
||||
|
||||
for (let i = 0; i < BATCH_SIZE; i++) {
|
||||
const obj = new TestObject();
|
||||
obj.set('batchField', `batch-${i}`);
|
||||
obj.set('timestamp', new Date());
|
||||
objects.push(obj);
|
||||
}
|
||||
|
||||
await Parse.Object.saveAll(objects);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Benchmark: User Signup
|
||||
*/
|
||||
async function benchmarkUserSignup(name) {
|
||||
let counter = 0;
|
||||
|
||||
return measureOperation({
|
||||
name,
|
||||
iterations: 500,
|
||||
operation: async () => {
|
||||
counter++;
|
||||
const user = new Parse.User();
|
||||
user.set('username', `benchmark_user_${Date.now()}_${counter}`);
|
||||
user.set('password', 'benchmark_password');
|
||||
user.set('email', `benchmark${counter}@example.com`);
|
||||
await user.signUp();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Benchmark: User Login
|
||||
*/
|
||||
async function benchmarkUserLogin(name) {
|
||||
// Setup: Create test users
|
||||
const users = [];
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const user = new Parse.User();
|
||||
user.set('username', `benchmark_login_user_${i}`);
|
||||
user.set('password', 'benchmark_password');
|
||||
user.set('email', `login${i}@example.com`);
|
||||
await user.signUp();
|
||||
users.push({ username: user.get('username'), password: 'benchmark_password' });
|
||||
await Parse.User.logOut();
|
||||
}
|
||||
|
||||
let counter = 0;
|
||||
|
||||
return measureOperation({
|
||||
name,
|
||||
iterations: 500,
|
||||
operation: async () => {
|
||||
const userCreds = users[counter++ % users.length];
|
||||
await Parse.User.logIn(userCreds.username, userCreds.password);
|
||||
await Parse.User.logOut();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Benchmark: Query with Include (Parallel Pointers)
|
||||
* Tests the performance improvement when fetching multiple pointers at the same level.
|
||||
*/
|
||||
async function benchmarkQueryWithIncludeParallel(name) {
|
||||
const PointerAClass = Parse.Object.extend('PointerA');
|
||||
const PointerBClass = Parse.Object.extend('PointerB');
|
||||
const PointerCClass = Parse.Object.extend('PointerC');
|
||||
const RootClass = Parse.Object.extend('Root');
|
||||
|
||||
// Create pointer objects
|
||||
const pointerAObjects = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const obj = new PointerAClass();
|
||||
obj.set('name', `pointerA-${i}`);
|
||||
pointerAObjects.push(obj);
|
||||
}
|
||||
await Parse.Object.saveAll(pointerAObjects);
|
||||
|
||||
const pointerBObjects = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const obj = new PointerBClass();
|
||||
obj.set('name', `pointerB-${i}`);
|
||||
pointerBObjects.push(obj);
|
||||
}
|
||||
await Parse.Object.saveAll(pointerBObjects);
|
||||
|
||||
const pointerCObjects = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const obj = new PointerCClass();
|
||||
obj.set('name', `pointerC-${i}`);
|
||||
pointerCObjects.push(obj);
|
||||
}
|
||||
await Parse.Object.saveAll(pointerCObjects);
|
||||
|
||||
// Create Root objects with multiple pointers at the same level
|
||||
const rootObjects = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const obj = new RootClass();
|
||||
obj.set('name', `root-${i}`);
|
||||
obj.set('pointerA', pointerAObjects[i % pointerAObjects.length]);
|
||||
obj.set('pointerB', pointerBObjects[i % pointerBObjects.length]);
|
||||
obj.set('pointerC', pointerCObjects[i % pointerCObjects.length]);
|
||||
rootObjects.push(obj);
|
||||
}
|
||||
await Parse.Object.saveAll(rootObjects);
|
||||
|
||||
return measureOperation({
|
||||
name,
|
||||
skipWarmup: true,
|
||||
dbLatency: 100,
|
||||
iterations: 100,
|
||||
operation: async () => {
|
||||
const query = new Parse.Query('Root');
|
||||
// Include multiple pointers at the same level - should fetch in parallel
|
||||
query.include(['pointerA', 'pointerB', 'pointerC']);
|
||||
await query.find();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Benchmark: Query with Include (Nested Pointers with Parallel Leaf Nodes)
|
||||
* Tests the PR's optimization for parallel fetching at each nested level.
|
||||
* Pattern: p1.p2.p3, p1.p2.p4, p1.p2.p5
|
||||
* After fetching p2, we know the objectIds and can fetch p3, p4, p5 in parallel.
|
||||
*/
|
||||
async function benchmarkQueryWithIncludeNested(name) {
|
||||
const Level3AClass = Parse.Object.extend('Level3A');
|
||||
const Level3BClass = Parse.Object.extend('Level3B');
|
||||
const Level3CClass = Parse.Object.extend('Level3C');
|
||||
const Level2Class = Parse.Object.extend('Level2');
|
||||
const Level1Class = Parse.Object.extend('Level1');
|
||||
const RootClass = Parse.Object.extend('Root');
|
||||
|
||||
// Create Level3 objects (leaf nodes)
|
||||
const level3AObjects = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const obj = new Level3AClass();
|
||||
obj.set('name', `level3A-${i}`);
|
||||
level3AObjects.push(obj);
|
||||
}
|
||||
await Parse.Object.saveAll(level3AObjects);
|
||||
|
||||
const level3BObjects = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const obj = new Level3BClass();
|
||||
obj.set('name', `level3B-${i}`);
|
||||
level3BObjects.push(obj);
|
||||
}
|
||||
await Parse.Object.saveAll(level3BObjects);
|
||||
|
||||
const level3CObjects = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const obj = new Level3CClass();
|
||||
obj.set('name', `level3C-${i}`);
|
||||
level3CObjects.push(obj);
|
||||
}
|
||||
await Parse.Object.saveAll(level3CObjects);
|
||||
|
||||
// Create Level2 objects pointing to multiple Level3 objects
|
||||
const level2Objects = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const obj = new Level2Class();
|
||||
obj.set('name', `level2-${i}`);
|
||||
obj.set('level3A', level3AObjects[i % level3AObjects.length]);
|
||||
obj.set('level3B', level3BObjects[i % level3BObjects.length]);
|
||||
obj.set('level3C', level3CObjects[i % level3CObjects.length]);
|
||||
level2Objects.push(obj);
|
||||
}
|
||||
await Parse.Object.saveAll(level2Objects);
|
||||
|
||||
// Create Level1 objects pointing to Level2
|
||||
const level1Objects = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const obj = new Level1Class();
|
||||
obj.set('name', `level1-${i}`);
|
||||
obj.set('level2', level2Objects[i % level2Objects.length]);
|
||||
level1Objects.push(obj);
|
||||
}
|
||||
await Parse.Object.saveAll(level1Objects);
|
||||
|
||||
// Create Root objects pointing to Level1
|
||||
const rootObjects = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const obj = new RootClass();
|
||||
obj.set('name', `root-${i}`);
|
||||
obj.set('level1', level1Objects[i % level1Objects.length]);
|
||||
rootObjects.push(obj);
|
||||
}
|
||||
await Parse.Object.saveAll(rootObjects);
|
||||
|
||||
return measureOperation({
|
||||
name,
|
||||
skipWarmup: true,
|
||||
dbLatency: 100,
|
||||
iterations: 100,
|
||||
operation: async () => {
|
||||
const query = new Parse.Query('Root');
|
||||
// After fetching level1.level2, the PR should fetch level3A, level3B, level3C in parallel
|
||||
query.include(['level1.level2.level3A', 'level1.level2.level3B', 'level1.level2.level3C']);
|
||||
await query.find();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Benchmark: Object.save with nested data (denylist scanning)
|
||||
*
|
||||
* Measures create latency for objects with deeply nested structures containing
|
||||
* multiple sibling objects at each level. This exercises the requestKeywordDenylist
|
||||
* scanner (objectContainsKeyValue) which must traverse all keys and nested values.
|
||||
*/
|
||||
async function benchmarkObjectCreateNestedDenylist(name) {
|
||||
let counter = 0;
|
||||
|
||||
return measureOperation({
|
||||
name,
|
||||
iterations: 1_000,
|
||||
operation: async () => {
|
||||
const TestObject = Parse.Object.extend('BenchmarkDenylist');
|
||||
const obj = new TestObject();
|
||||
const idx = counter++;
|
||||
obj.set('nested', {
|
||||
meta1: { info: { detail: `value-${idx}` } },
|
||||
meta2: { info: { detail: `value-${idx}` } },
|
||||
meta3: { info: { detail: `value-${idx}` } },
|
||||
tags: ['a', 'b', 'c'],
|
||||
config: {
|
||||
setting1: { enabled: true, params: { x: 1 } },
|
||||
setting2: { enabled: false, params: { y: 2 } },
|
||||
},
|
||||
});
|
||||
await obj.save();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Run all benchmarks
|
||||
*/
|
||||
async function runBenchmarks() {
|
||||
logInfo('Starting Parse Server Performance Benchmarks...');
|
||||
|
||||
let server;
|
||||
|
||||
try {
|
||||
// Initialize Parse Server
|
||||
logInfo('Initializing Parse Server...');
|
||||
server = await initializeParseServer();
|
||||
|
||||
// Wait for server to be ready
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
const results = [];
|
||||
|
||||
// Define all benchmarks to run
|
||||
const benchmarks = [
|
||||
{ name: 'Object.save (create)', fn: benchmarkObjectCreate },
|
||||
{ name: 'Object.save (update)', fn: benchmarkObjectUpdate },
|
||||
{ name: 'Object.saveAll (batch save)', fn: benchmarkBatchSave },
|
||||
{ name: 'Query.get (by objectId)', fn: benchmarkObjectRead },
|
||||
{ name: 'Query.find (simple query)', fn: benchmarkSimpleQuery },
|
||||
{ name: 'User.signUp', fn: benchmarkUserSignup },
|
||||
{ name: 'User.login', fn: benchmarkUserLogin },
|
||||
{ name: 'Query.include (parallel pointers)', fn: benchmarkQueryWithIncludeParallel },
|
||||
{ name: 'Query.include (nested pointers)', fn: benchmarkQueryWithIncludeNested },
|
||||
{ name: 'Object.save (nested data, denylist scan)', fn: benchmarkObjectCreateNestedDenylist },
|
||||
];
|
||||
|
||||
// Run each benchmark with database cleanup
|
||||
for (const benchmark of benchmarks) {
|
||||
logInfo(`\nRunning benchmark '${benchmark.name}'...`);
|
||||
resetParseServer();
|
||||
await cleanupDatabase();
|
||||
results.push(await benchmark.fn(benchmark.name));
|
||||
}
|
||||
|
||||
// Output results in github-action-benchmark format (stdout)
|
||||
logInfo(JSON.stringify(results, null, 2));
|
||||
|
||||
// Output summary to stderr for visibility
|
||||
logInfo('Benchmarks completed successfully!');
|
||||
logInfo('Summary:');
|
||||
results.forEach(result => {
|
||||
logInfo(` ${result.name}: ${result.value.toFixed(2)} ${result.unit} (${result.extra})`);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logError('Error running benchmarks:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
// Cleanup
|
||||
if (mongoClient) {
|
||||
await mongoClient.close();
|
||||
}
|
||||
if (server) {
|
||||
server.close();
|
||||
}
|
||||
// Give some time for cleanup
|
||||
setTimeout(() => process.exit(0), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
// Run benchmarks if executed directly
|
||||
if (require.main === module) {
|
||||
runBenchmarks();
|
||||
}
|
||||
|
||||
module.exports = { runBenchmarks };
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,133 +1,3 @@
|
||||
# [7.4.0-beta.1](https://github.com/parse-community/parse-server/compare/7.3.0...7.4.0-beta.1) (2024-12-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `Parse.Query.distinct` fails due to invalid aggregate stage 'hint' ([#9295](https://github.com/parse-community/parse-server/issues/9295)) ([5f66c6a](https://github.com/parse-community/parse-server/commit/5f66c6a075cbe1cdaf9d1b108ee65af8ae596b89))
|
||||
* Security upgrade cross-spawn from 7.0.3 to 7.0.6 ([#9444](https://github.com/parse-community/parse-server/issues/9444)) ([3d034e0](https://github.com/parse-community/parse-server/commit/3d034e0a993e3e5bd9bb96a7e382bb3464f1eb68))
|
||||
* Security upgrade fast-xml-parser from 4.4.0 to 4.4.1 ([#9262](https://github.com/parse-community/parse-server/issues/9262)) ([992d39d](https://github.com/parse-community/parse-server/commit/992d39d508f230c774dcb764d1d907ec8887e6c5))
|
||||
* Security upgrade node from 20.14.0-alpine3.20 to 20.17.0-alpine3.20 ([#9300](https://github.com/parse-community/parse-server/issues/9300)) ([15bb17d](https://github.com/parse-community/parse-server/commit/15bb17d87153bf0d38f08fe4c720da29a204b36b))
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for MongoDB 8 ([#9269](https://github.com/parse-community/parse-server/issues/9269)) ([4756c66](https://github.com/parse-community/parse-server/commit/4756c66cd9f55afa1621d1a3f6fa850ed605cb53))
|
||||
* Add support for PostGIS 3.5 ([#9354](https://github.com/parse-community/parse-server/issues/9354)) ([8ea3538](https://github.com/parse-community/parse-server/commit/8ea35382db3436d54ab59bd30706705564b0985c))
|
||||
* Add support for Postgres 17 ([#9324](https://github.com/parse-community/parse-server/issues/9324)) ([fa2ee31](https://github.com/parse-community/parse-server/commit/fa2ee3196e4319a142b3838bb947c98dcba5d5cb))
|
||||
* Upgrade @parse/push-adapter from 6.7.1 to 6.8.0 ([#9489](https://github.com/parse-community/parse-server/issues/9489)) ([286aa66](https://github.com/parse-community/parse-server/commit/286aa664ac8830d36c3e70d2316917d15f0b6df5))
|
||||
|
||||
# [7.3.0-beta.1](https://github.com/parse-community/parse-server/compare/7.2.0...7.3.0-beta.1) (2024-10-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Custom object ID allows to acquire role privileges ([GHSA-8xq9-g7ch-35hg](https://github.com/parse-community/parse-server/security/advisories/GHSA-8xq9-g7ch-35hg)) ([#9317](https://github.com/parse-community/parse-server/issues/9317)) ([13ee52f](https://github.com/parse-community/parse-server/commit/13ee52f0d19ef3a3524b3d79aea100e587eb3cfc))
|
||||
* Parse Server `databaseOptions` nested keys incorrectly identified as invalid ([#9213](https://github.com/parse-community/parse-server/issues/9213)) ([77206d8](https://github.com/parse-community/parse-server/commit/77206d804443cfc1618c24f8961bd677de9920c0))
|
||||
* Parse Server installation fails due to post install script incorrectly parsing required min. Node version ([#9216](https://github.com/parse-community/parse-server/issues/9216)) ([0fa82a5](https://github.com/parse-community/parse-server/commit/0fa82a54fe38ec14e8054339285d3db71a8624c8))
|
||||
* Parse Server option `maxLogFiles` doesn't recognize day duration literals such as `1d` to mean 1 day ([#9215](https://github.com/parse-community/parse-server/issues/9215)) ([0319cee](https://github.com/parse-community/parse-server/commit/0319cee2dbf65e90bad377af1ed14ea25c595bf5))
|
||||
* Security upgrade path-to-regexp from 6.2.1 to 6.3.0 ([#9314](https://github.com/parse-community/parse-server/issues/9314)) ([8b7fe69](https://github.com/parse-community/parse-server/commit/8b7fe699c1c376ecd8cc1c97cce8e704ee41f28a))
|
||||
|
||||
### Features
|
||||
|
||||
* Add atomic operations for Cloud Config parameters ([#9219](https://github.com/parse-community/parse-server/issues/9219)) ([35cadf9](https://github.com/parse-community/parse-server/commit/35cadf9b8324879fb7309ba5d7ea46f2c722d614))
|
||||
* Add Cloud Code triggers `Parse.Cloud.beforeSave` and `Parse.Cloud.afterSave` for Parse Config ([#9232](https://github.com/parse-community/parse-server/issues/9232)) ([90a1e4a](https://github.com/parse-community/parse-server/commit/90a1e4a200423d644efb3f0ba2fba4b99f5cf954))
|
||||
* Add Node 22 support ([#9187](https://github.com/parse-community/parse-server/issues/9187)) ([7778471](https://github.com/parse-community/parse-server/commit/7778471999c7e42236ce404229660d80ecc2acd6))
|
||||
* Add support for asynchronous invocation of `FilesAdapter.getFileLocation` ([#9271](https://github.com/parse-community/parse-server/issues/9271)) ([1a2da40](https://github.com/parse-community/parse-server/commit/1a2da4055abe831b3017172fb75e16d7a8093873))
|
||||
|
||||
# [7.2.0-beta.1](https://github.com/parse-community/parse-server/compare/7.1.0...7.2.0-beta.1) (2024-07-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Invalid push notification tokens are not cleaned up from database for FCM API v2 ([#9173](https://github.com/parse-community/parse-server/issues/9173)) ([284da09](https://github.com/parse-community/parse-server/commit/284da09f4546356b37511a589fb5f64a3efffe79))
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for dot notation on array fields of Parse Object ([#9115](https://github.com/parse-community/parse-server/issues/9115)) ([cf4c880](https://github.com/parse-community/parse-server/commit/cf4c8807b9da87a0a5f9c94e5bdfcf17cda80cf4))
|
||||
* Upgrade to @parse/push-adapter 6.4.0 ([#9182](https://github.com/parse-community/parse-server/issues/9182)) ([ef1634b](https://github.com/parse-community/parse-server/commit/ef1634bf1f360429108d29b08032fc7961ff96a1))
|
||||
* Upgrade to Parse JS SDK 5.3.0 ([#9180](https://github.com/parse-community/parse-server/issues/9180)) ([dca187f](https://github.com/parse-community/parse-server/commit/dca187f91b93cbb362b22a3fb9ee38451799ff13))
|
||||
|
||||
# [7.1.0-beta.1](https://github.com/parse-community/parse-server/compare/7.0.0...7.1.0-beta.1) (2024-06-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `Parse.Cloud.startJob` and `Parse.Push.send` not returning status ID when setting Parse Server option `directAccess: true` ([#8766](https://github.com/parse-community/parse-server/issues/8766)) ([5b0efb2](https://github.com/parse-community/parse-server/commit/5b0efb22efe94c47f243cf8b1e6407ed5c5a67d3))
|
||||
* `Required` option not handled correctly for special fields (File, GeoPoint, Polygon) on GraphQL API mutations ([#8915](https://github.com/parse-community/parse-server/issues/8915)) ([907ad42](https://github.com/parse-community/parse-server/commit/907ad4267c228d26cfcefe7848b30ce85ba7ff8f))
|
||||
* Facebook Limited Login not working due to incorrect domain in JWT validation ([#9122](https://github.com/parse-community/parse-server/issues/9122)) ([9d0bd2b](https://github.com/parse-community/parse-server/commit/9d0bd2badd6e5f7429d1af00b118225752e5d86a))
|
||||
* Live query throws error when constraint `notEqualTo` is set to `null` ([#8835](https://github.com/parse-community/parse-server/issues/8835)) ([11d3e48](https://github.com/parse-community/parse-server/commit/11d3e484df862224c15d20f6171514948981ea90))
|
||||
* Parse Server option `extendSessionOnUse` not working for session lengths < 24 hours ([#9113](https://github.com/parse-community/parse-server/issues/9113)) ([0a054e6](https://github.com/parse-community/parse-server/commit/0a054e6b541fd5ab470bf025665f5f7d2acedaa0))
|
||||
* Rate limiting can fail when using Parse Server option `rateLimit.redisUrl` with clusters ([#8632](https://github.com/parse-community/parse-server/issues/8632)) ([c277739](https://github.com/parse-community/parse-server/commit/c27773962399f8e27691e3b8087e7e1d59516efd))
|
||||
* SQL injection when using Parse Server with PostgreSQL; fixes security vulnerability [GHSA-c2hr-cqg6-8j6r](https://github.com/parse-community/parse-server/security/advisories/GHSA-c2hr-cqg6-8j6r) ([#9167](https://github.com/parse-community/parse-server/issues/9167)) ([2edf1e4](https://github.com/parse-community/parse-server/commit/2edf1e4c0363af01e97a7fbc97694f851b7d1ff3))
|
||||
|
||||
### Features
|
||||
|
||||
* Add `silent` log level for Cloud Code ([#8803](https://github.com/parse-community/parse-server/issues/8803)) ([5f81efb](https://github.com/parse-community/parse-server/commit/5f81efb42964c4c2fa8bcafee9446a0122e3ce21))
|
||||
* Add server security check status `security.enableCheck` to Features Router ([#8679](https://github.com/parse-community/parse-server/issues/8679)) ([b07ec15](https://github.com/parse-community/parse-server/commit/b07ec153825882e97cc48dc84072c7f549f3238b))
|
||||
* Prevent Parse Server start in case of unknown option in server configuration ([#8987](https://github.com/parse-community/parse-server/issues/8987)) ([8758e6a](https://github.com/parse-community/parse-server/commit/8758e6abb9dbb68757bddcbd332ad25100c24a0e))
|
||||
* Upgrade to @parse/push-adapter 6.0.0 ([#9066](https://github.com/parse-community/parse-server/issues/9066)) ([18bdbf8](https://github.com/parse-community/parse-server/commit/18bdbf89c53a57648891ef582614ba7c2941e587))
|
||||
* Upgrade to @parse/push-adapter 6.2.0 ([#9127](https://github.com/parse-community/parse-server/issues/9127)) ([ca20496](https://github.com/parse-community/parse-server/commit/ca20496f28e5ec1294a7a23c8559df82b79b2a04))
|
||||
* Upgrade to Parse JS SDK 5.2.0 ([#9128](https://github.com/parse-community/parse-server/issues/9128)) ([665b8d5](https://github.com/parse-community/parse-server/commit/665b8d52d6cf5275179a5e1fb132c934edb53ecc))
|
||||
|
||||
# [7.0.0-beta.1](https://github.com/parse-community/parse-server/compare/6.5.0-beta.1...7.0.0-beta.1) (2024-03-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* CacheAdapter does not connect when using a CacheAdapter with a JSON config ([#8633](https://github.com/parse-community/parse-server/issues/8633)) ([720d24e](https://github.com/parse-community/parse-server/commit/720d24e18540da35d50957f17be878316ec30318))
|
||||
* Conditional email verification not working in some cases if `verifyUserEmails`, `preventLoginWithUnverifiedEmail` set to functions ([#8838](https://github.com/parse-community/parse-server/issues/8838)) ([8e7a6b1](https://github.com/parse-community/parse-server/commit/8e7a6b1480c0117e6c73e7adc5a6619115a04e85))
|
||||
* Deny request if master key is not set in Parse Server option `masterKeyIps` regardless of ACL and CLP ([#8957](https://github.com/parse-community/parse-server/issues/8957)) ([a7b5b38](https://github.com/parse-community/parse-server/commit/a7b5b38418cbed9be3f4a7665f25b97f592663e1))
|
||||
* Docker image not published to Docker Hub on new release ([#8905](https://github.com/parse-community/parse-server/issues/8905)) ([a2ac8d1](https://github.com/parse-community/parse-server/commit/a2ac8d133c71cd7b61e5ef59c4be915cfea85db6))
|
||||
* Docker version releases by removing arm/v6 and arm/v7 support ([#8976](https://github.com/parse-community/parse-server/issues/8976)) ([1f62dd0](https://github.com/parse-community/parse-server/commit/1f62dd0f4e107b22a387692558a042ee26ce8703))
|
||||
* GraphQL file upload fails in case of use of pointer or relation ([#8721](https://github.com/parse-community/parse-server/issues/8721)) ([1aba638](https://github.com/parse-community/parse-server/commit/1aba6382c873fb489d4a898d301e6da9fb6aa61b))
|
||||
* Improve PostgreSQL injection detection; fixes security vulnerability [GHSA-6927-3vr9-fxf2](https://github.com/parse-community/parse-server/security/advisories/GHSA-6927-3vr9-fxf2) which affects Parse Server deployments using a Postgres database ([#8961](https://github.com/parse-community/parse-server/issues/8961)) ([cbefe77](https://github.com/parse-community/parse-server/commit/cbefe770a7260b54748a058b8a7389937dc35833))
|
||||
* Incomplete user object in `verifyEmail` function if both username and email are changed ([#8889](https://github.com/parse-community/parse-server/issues/8889)) ([1eb95ae](https://github.com/parse-community/parse-server/commit/1eb95aeb41a96250e582d79a703f6adcb403c08b))
|
||||
* Parse Server option `emailVerifyTokenReuseIfValid: true` generates new token on every email verification request ([#8885](https://github.com/parse-community/parse-server/issues/8885)) ([0023ce4](https://github.com/parse-community/parse-server/commit/0023ce448a5e9423337d0e1a25648bde1156bc95))
|
||||
* Parse Server option `fileExtensions` default value rejects file extensions that are less than 3 or more than 4 characters long ([#8699](https://github.com/parse-community/parse-server/issues/8699)) ([2760381](https://github.com/parse-community/parse-server/commit/276038118377c2b22381bcd8d30337203822121b))
|
||||
* Server crashes on invalid Cloud Function or Cloud Job name; fixes security vulnerability [GHSA-6hh7-46r2-vf29](https://github.com/parse-community/parse-server/security/advisories/GHSA-6hh7-46r2-vf29) ([#9024](https://github.com/parse-community/parse-server/issues/9024)) ([9f6e342](https://github.com/parse-community/parse-server/commit/9f6e3429d3b326cf4e2994733c618d08032fac6e))
|
||||
* Server crashes when receiving an array of `Parse.Pointer` in the request body ([#8784](https://github.com/parse-community/parse-server/issues/8784)) ([66e3603](https://github.com/parse-community/parse-server/commit/66e36039d8af654cfa0284666c0ddd94975dcb52))
|
||||
* Username is `undefined` in email verification link on email change ([#8887](https://github.com/parse-community/parse-server/issues/8887)) ([e315c13](https://github.com/parse-community/parse-server/commit/e315c137bf41bedfa8f0df537f2c3f6ab45b7e60))
|
||||
|
||||
### Features
|
||||
|
||||
* Add `installationId` to arguments for `verifyUserEmails`, `preventLoginWithUnverifiedEmail` ([#8836](https://github.com/parse-community/parse-server/issues/8836)) ([a22dbe1](https://github.com/parse-community/parse-server/commit/a22dbe16d5ac0090608f6caaf0ebd134925b7fd4))
|
||||
* Add `installationId`, `ip`, `resendRequest` to arguments passed to `verifyUserEmails` on verification email request ([#8873](https://github.com/parse-community/parse-server/issues/8873)) ([8adcbee](https://github.com/parse-community/parse-server/commit/8adcbee11283d3e95179ca2047e2615f52c18806))
|
||||
* Add `Parse.User` as function parameter to Parse Server options `verifyUserEmails`, `preventLoginWithUnverifiedEmail` on login ([#8850](https://github.com/parse-community/parse-server/issues/8850)) ([972f630](https://github.com/parse-community/parse-server/commit/972f6300163b3cd7d95eeb95986e8322c95f821c))
|
||||
* Add password validation via POST request for user with unverified email using master key and option `ignoreEmailVerification` ([#8895](https://github.com/parse-community/parse-server/issues/8895)) ([633a9d2](https://github.com/parse-community/parse-server/commit/633a9d25e4253e2125bc93c02ee8a37e0f5f7b83))
|
||||
* Add support for MongoDB 7 ([#8761](https://github.com/parse-community/parse-server/issues/8761)) ([3de8494](https://github.com/parse-community/parse-server/commit/3de8494a221991dfd10a74e0a2dc89576265c9b7))
|
||||
* Add support for MongoDB query comment ([#8928](https://github.com/parse-community/parse-server/issues/8928)) ([2170962](https://github.com/parse-community/parse-server/commit/2170962a50fa353ed85eda3f11dce7ee3647b087))
|
||||
* Add support for Node 20, drop support for Node 14, 16 ([#8907](https://github.com/parse-community/parse-server/issues/8907)) ([ced4872](https://github.com/parse-community/parse-server/commit/ced487246ea0ef72a8aa014991f003209b34841e))
|
||||
* Add support for Postgres 16 ([#8898](https://github.com/parse-community/parse-server/issues/8898)) ([99489b2](https://github.com/parse-community/parse-server/commit/99489b22e4f0982e6cb39992974b51aa8d3a31e4))
|
||||
* Allow `Parse.Session.current` on expired session token instead of throwing error ([#8722](https://github.com/parse-community/parse-server/issues/8722)) ([f9dde4a](https://github.com/parse-community/parse-server/commit/f9dde4a9f8a90c63f71172c9bc515b0f6c6d2e4a))
|
||||
* Deprecation DEPPS5: Config option `allowClientClassCreation` defaults to `false` ([#8849](https://github.com/parse-community/parse-server/issues/8849)) ([29624e0](https://github.com/parse-community/parse-server/commit/29624e0fae17161cd412ae58d35a195cfa286cad))
|
||||
* Deprecation DEPPS6: Authentication adapters disabled by default ([#8858](https://github.com/parse-community/parse-server/issues/8858)) ([0cf58eb](https://github.com/parse-community/parse-server/commit/0cf58eb8d60c8e5f485764e154f3214c49eee430))
|
||||
* Deprecation DEPPS7: Remove deprecated Cloud Code file trigger syntax ([#8855](https://github.com/parse-community/parse-server/issues/8855)) ([4e6a375](https://github.com/parse-community/parse-server/commit/4e6a375b5184ae0f7aa256a921eca4021c609435))
|
||||
* Deprecation DEPPS8: Parse Server option `allowExpiredAuthDataToken` defaults to `false` ([#8860](https://github.com/parse-community/parse-server/issues/8860)) ([e29845f](https://github.com/parse-community/parse-server/commit/e29845f8dacac09ce3093d75c0d92330c24389e8))
|
||||
* Deprecation DEPPS9: LiveQuery `fields` option is renamed to `keys` ([#8852](https://github.com/parse-community/parse-server/issues/8852)) ([38983e8](https://github.com/parse-community/parse-server/commit/38983e8e9b5cdbd006f311a2338103624137d013))
|
||||
* Node process exits with error code 1 on uncaught exception to allow custom uncaught exception handling ([#8894](https://github.com/parse-community/parse-server/issues/8894)) ([70c280c](https://github.com/parse-community/parse-server/commit/70c280ca578ff28b5acf92f37fbe06d42a5b34ca))
|
||||
* Switch GraphQL server from Yoga v2 to Apollo v4 ([#8959](https://github.com/parse-community/parse-server/issues/8959)) ([105ae7c](https://github.com/parse-community/parse-server/commit/105ae7c8a57d5a650b243174a80c26bf6db16e28))
|
||||
* Upgrade Parse Server Push Adapter to 5.0.2 ([#8813](https://github.com/parse-community/parse-server/issues/8813)) ([6ef1986](https://github.com/parse-community/parse-server/commit/6ef1986c03a1d84b7e11c05851e5bf9688d88740))
|
||||
* Upgrade to Parse JS SDK 5 ([#9022](https://github.com/parse-community/parse-server/issues/9022)) ([ad4aa83](https://github.com/parse-community/parse-server/commit/ad4aa83983205a0e27639f6ee6a4a5963b67e4b8))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* Improved IP validation performance for `masterKeyIPs`, `maintenanceKeyIPs` ([#8510](https://github.com/parse-community/parse-server/issues/8510)) ([b87daba](https://github.com/parse-community/parse-server/commit/b87daba0671a1b0b7b8d63bc671d665c91a04522))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* The Parse Server option `allowClientClassCreation` defaults to `false`. ([29624e0](29624e0))
|
||||
* A request using the master key will now be rejected as unauthorized if the IP from which the request originates is not set in the Parse Server option `masterKeyIps`, even if the request does not require the master key permission, for example for a public object in a public class class. ([a7b5b38](a7b5b38))
|
||||
* Node process now exits with code 1 on uncaught exceptions, enabling custom handlers that were blocked by Parse Server's default behavior of re-throwing errors. This change may lead to automatic process restarts by the environment, unlike before. ([70c280c](70c280c))
|
||||
* Authentication adapters are disabled by default; to use an authentication adapter it needs to be explicitly enabled in the Parse Server authentication adapter option `auth.<provider>.enabled: true` ([0cf58eb](0cf58eb))
|
||||
* Parse Server option `allowExpiredAuthDataToken` defaults to `false`; a 3rd party authentication token will be validated every time the user tries to log in and the login will fail if the token has expired; the effect of this change may differ for different authentication adapters, depending on the token lifetime and the token refresh logic of the adapter ([e29845f](e29845f))
|
||||
* LiveQuery `fields` option is renamed to `keys` ([38983e8](38983e8))
|
||||
* Cloud Code file trigger syntax has been aligned with object trigger syntax, for example `Parse.Cloud.beforeDeleteFile'` has been changed to `Parse.Cloud.beforeDelete(Parse.File, (request) => {})'` ([4e6a375](4e6a375))
|
||||
* Removes support for Node 14 and 16 ([ced4872](ced4872))
|
||||
* Removes support for Postgres 11 and 12 ([99489b2](99489b2))
|
||||
* The `Parse.User` passed as argument if `verifyUserEmails` is set to a function is renamed from `user` to `object` for consistency with invocations of `verifyUserEmails` on signup or login; the user object is not a plain JavaScript object anymore but an instance of `Parse.User` ([8adcbee](8adcbee))
|
||||
* `Parse.Session.current()` no longer throws an error if the session token is expired, but instead returns the session token with its expiration date to allow checking its validity ([f9dde4a](f9dde4a))
|
||||
* `Parse.Query` no longer supports the BSON type `code`; although this feature was never officially documented, its removal is announced as a breaking change to protect deployments where it might be in use. ([3de8494](3de8494))
|
||||
|
||||
# [6.5.0-beta.1](https://github.com/parse-community/parse-server/compare/6.4.0...6.5.0-beta.1) (2023-11-16)
|
||||
|
||||
|
||||
|
||||
+24
-638
@@ -1,700 +1,86 @@
|
||||
## [8.6.53](https://github.com/parse-community/parse-server/compare/8.6.52...8.6.53) (2026-03-20)
|
||||
## [6.5.11](https://github.com/parse-community/parse-server/compare/6.5.10...6.5.11) (2024-10-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* LiveQuery bypasses CLP pointer permission enforcement ([GHSA-fph2-r4qg-9576](https://github.com/parse-community/parse-server/security/advisories/GHSA-fph2-r4qg-9576)) ([#10252](https://github.com/parse-community/parse-server/issues/10252)) ([976dad1](https://github.com/parse-community/parse-server/commit/976dad109f3fe3fbd0a3a35ef62e7a5d35eb0bee))
|
||||
* Security upgrade to path-to-regexp 6.2.2 ([#9365](https://github.com/parse-community/parse-server/issues/9365)) ([dfbafa7](https://github.com/parse-community/parse-server/commit/dfbafa702507da0d20667eabe8fdc835ca30c8e8))
|
||||
|
||||
## [8.6.52](https://github.com/parse-community/parse-server/compare/8.6.51...8.6.52) (2026-03-19)
|
||||
## [6.5.10](https://github.com/parse-community/parse-server/compare/6.5.9...6.5.10) (2024-10-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Auth provider validation bypass on login via partial authData ([GHSA-pfj7-wv7c-22pr](https://github.com/parse-community/parse-server/security/advisories/GHSA-pfj7-wv7c-22pr)) ([#10247](https://github.com/parse-community/parse-server/issues/10247)) ([8d7df56](https://github.com/parse-community/parse-server/commit/8d7df5639c4a35768fe8b78b4580b30e8a74721c))
|
||||
* Security upgrade express from 4.21.0 to 4.21.1 ([#9339](https://github.com/parse-community/parse-server/issues/9339)) ([97627ee](https://github.com/parse-community/parse-server/commit/97627ee450f5abe04c92609f4ae2e63c49060d6a))
|
||||
|
||||
## [8.6.51](https://github.com/parse-community/parse-server/compare/8.6.50...8.6.51) (2026-03-19)
|
||||
## [6.5.9](https://github.com/parse-community/parse-server/compare/6.5.8...6.5.9) (2024-10-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Email verification resend page leaks user existence (GHSA-h29g-q5c2-9h4f) ([#10243](https://github.com/parse-community/parse-server/issues/10243)) ([967aa57](https://github.com/parse-community/parse-server/commit/967aa57732202009b2389ce9ecb3130d53d657e5))
|
||||
* Custom object ID allows to acquire role privileges ([GHSA-8xq9-g7ch-35hg](https://github.com/parse-community/parse-server/security/advisories/GHSA-8xq9-g7ch-35hg)) ([#9318](https://github.com/parse-community/parse-server/issues/9318)) ([1bfbccf](https://github.com/parse-community/parse-server/commit/1bfbccf9ee7ea77533b2b2aa7c4c69f3bd35e66f))
|
||||
|
||||
## [8.6.50](https://github.com/parse-community/parse-server/compare/8.6.49...8.6.50) (2026-03-17)
|
||||
## [6.5.8](https://github.com/parse-community/parse-server/compare/6.5.7...6.5.8) (2024-09-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Protected fields leak via LiveQuery afterEvent trigger ([GHSA-5hmj-jcgp-6hff](https://github.com/parse-community/parse-server/security/advisories/GHSA-5hmj-jcgp-6hff)) ([#10233](https://github.com/parse-community/parse-server/issues/10233)) ([743324e](https://github.com/parse-community/parse-server/commit/743324e71fa2a6693bea78c4589cf2211b210eb6))
|
||||
* Various vulnerabilities related to cross-site scripting ([#9310](https://github.com/parse-community/parse-server/issues/9310)) ([d5290d4](https://github.com/parse-community/parse-server/commit/d5290d46e5ff9237970ae1ac2d2df4051cbf53e5))
|
||||
|
||||
## [8.6.49](https://github.com/parse-community/parse-server/compare/8.6.48...8.6.49) (2026-03-16)
|
||||
## [6.5.7](https://github.com/parse-community/parse-server/compare/6.5.6...6.5.7) (2024-06-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Empty authData bypasses credential requirement on signup ([GHSA-wjqw-r9x4-j59v](https://github.com/parse-community/parse-server/security/advisories/GHSA-wjqw-r9x4-j59v)) ([#10220](https://github.com/parse-community/parse-server/issues/10220)) ([b62336b](https://github.com/parse-community/parse-server/commit/b62336be06d06e3e9fbf3365354363e381603f58))
|
||||
* SQL injection when using Parse Server with PostgreSQL; fixes security vulnerability [GHSA-c2hr-cqg6-8j6r](https://github.com/parse-community/parse-server/security/advisories/GHSA-c2hr-cqg6-8j6r) ([#9168](https://github.com/parse-community/parse-server/issues/9168)) ([f332d54](https://github.com/parse-community/parse-server/commit/f332d54577608c5ad927255e06d8c694e2e0ff5b))
|
||||
|
||||
## [8.6.48](https://github.com/parse-community/parse-server/compare/8.6.47...8.6.48) (2026-03-16)
|
||||
## [6.5.6](https://github.com/parse-community/parse-server/compare/6.5.5...6.5.6) (2024-05-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Password reset token single-use bypass via concurrent requests ([GHSA-r3xq-68wh-gwvh](https://github.com/parse-community/parse-server/security/advisories/GHSA-r3xq-68wh-gwvh)) ([#10217](https://github.com/parse-community/parse-server/issues/10217)) ([83b4de0](https://github.com/parse-community/parse-server/commit/83b4de0b7ce722fac0ecbb5fe815e3da8fb6b8a0))
|
||||
* Facebook Limited Login not workind due to incorrect domain in JWT validation ([#9120](https://github.com/parse-community/parse-server/issues/9120)) ([0e92f76](https://github.com/parse-community/parse-server/commit/0e92f765e43d1e39285c5958d60cfd7fb76a3c90))
|
||||
|
||||
## [8.6.47](https://github.com/parse-community/parse-server/compare/8.6.46...8.6.47) (2026-03-15)
|
||||
## [6.5.5](https://github.com/parse-community/parse-server/compare/6.5.4...6.5.5) (2024-03-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Cloud function dispatch crashes server via prototype chain traversal ([GHSA-4263-jgmp-7pf4](https://github.com/parse-community/parse-server/security/advisories/GHSA-4263-jgmp-7pf4)) ([#10211](https://github.com/parse-community/parse-server/issues/10211)) ([8d8c760](https://github.com/parse-community/parse-server/commit/8d8c7604790f931531ac31bd88c98eb3d995b9c5))
|
||||
* Server crashes on invalid Cloud Function or Cloud Job name; fixes security vulnerability [GHSA-6hh7-46r2-vf29](https://github.com/parse-community/parse-server/security/advisories/GHSA-6hh7-46r2-vf29) ([#9023](https://github.com/parse-community/parse-server/issues/9023)) ([5ae6d6a](https://github.com/parse-community/parse-server/commit/5ae6d6a36d75c4511029f0ba5673ae4b2999179b))
|
||||
|
||||
## [8.6.46](https://github.com/parse-community/parse-server/compare/8.6.45...8.6.46) (2026-03-15)
|
||||
## [6.5.4](https://github.com/parse-community/parse-server/compare/6.5.3...6.5.4) (2024-03-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Revert accidental breaking default values for query complexity limits ([#10206](https://github.com/parse-community/parse-server/issues/10206)) ([a3a57c1](https://github.com/parse-community/parse-server/commit/a3a57c15077d0c9c902fd444de986915b942ab2f))
|
||||
* Server crashes when receiving an array of `Parse.Pointer` in the request body ([#9012](https://github.com/parse-community/parse-server/issues/9012)) ([8ff444d](https://github.com/parse-community/parse-server/commit/8ff444d42ef51bfe6808c4c3a5fe666bfe302ebe))
|
||||
|
||||
## [8.6.45](https://github.com/parse-community/parse-server/compare/8.6.44...8.6.45) (2026-03-15)
|
||||
## [6.5.3](https://github.com/parse-community/parse-server/compare/6.5.2...6.5.3) (2024-03-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Server crash via deeply nested query condition operators ([GHSA-9xp9-j92r-p88v](https://github.com/parse-community/parse-server/security/advisories/GHSA-9xp9-j92r-p88v)) ([#10203](https://github.com/parse-community/parse-server/issues/10203)) ([433fa8f](https://github.com/parse-community/parse-server/commit/433fa8fb19f813966871da8be874e1197a29b10b))
|
||||
* Security upgrade follow-redirects from 1.15.5 to 1.15.6 ([#9019](https://github.com/parse-community/parse-server/issues/9019)) ([422958e](https://github.com/parse-community/parse-server/commit/422958e246da6f13011776c8dde028a00fb821cb))
|
||||
|
||||
## [8.6.44](https://github.com/parse-community/parse-server/compare/8.6.43...8.6.44) (2026-03-14)
|
||||
## [6.5.2](https://github.com/parse-community/parse-server/compare/6.5.1...6.5.2) (2024-03-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Schema poisoning via prototype pollution in deep copy ([GHSA-9ccr-fpp6-78qf](https://github.com/parse-community/parse-server/security/advisories/GHSA-9ccr-fpp6-78qf)) ([#10201](https://github.com/parse-community/parse-server/issues/10201)) ([6aec8ea](https://github.com/parse-community/parse-server/commit/6aec8ea9e17375930e55406d0c62a429505924cc))
|
||||
* Security upgrade @parse/push-adapter from 5.1.0 to 5.1.1 ([#8975](https://github.com/parse-community/parse-server/issues/8975)) ([0fa0aab](https://github.com/parse-community/parse-server/commit/0fa0aabefe6bc9d356ee70be78dafc5fa22d4e17))
|
||||
|
||||
## [8.6.43](https://github.com/parse-community/parse-server/compare/8.6.42...8.6.43) (2026-03-14)
|
||||
## [6.5.1](https://github.com/parse-community/parse-server/compare/6.5.0...6.5.1) (2024-03-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* LiveQuery subscription with invalid regular expression crashes server ([GHSA-827p-g5x5-h86c](https://github.com/parse-community/parse-server/security/advisories/GHSA-827p-g5x5-h86c)) ([#10199](https://github.com/parse-community/parse-server/issues/10199)) ([522f008](https://github.com/parse-community/parse-server/commit/522f008f64f6a4ae3c0b9a299ee6a53947a9c1ea))
|
||||
* Security upgrade @parse/push-adapter from 5.0.2 to 5.1.0 ([#8972](https://github.com/parse-community/parse-server/issues/8972)) ([bba24dd](https://github.com/parse-community/parse-server/commit/bba24dd8279ebb8e4084a5f00fbe3ab9fe6c01b4))
|
||||
|
||||
## [8.6.42](https://github.com/parse-community/parse-server/compare/8.6.41...8.6.42) (2026-03-13)
|
||||
# [6.5.0](https://github.com/parse-community/parse-server/compare/6.4.0...6.5.0) (2024-03-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Session creation endpoint allows overwriting server-generated session fields ([GHSA-5v7g-9h8f-8pgg](https://github.com/parse-community/parse-server/security/advisories/GHSA-5v7g-9h8f-8pgg)) ([#10196](https://github.com/parse-community/parse-server/issues/10196)) ([2021b27](https://github.com/parse-community/parse-server/commit/2021b277e1ff1131cf79eb37bba07cc5fba872c7))
|
||||
|
||||
## [8.6.41](https://github.com/parse-community/parse-server/compare/8.6.40...8.6.41) (2026-03-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Stored XSS filter bypass via Content-Type MIME parameter and missing XML extension blocklist entries ([GHSA-42ph-pf9q-cr72](https://github.com/parse-community/parse-server/security/advisories/GHSA-42ph-pf9q-cr72)) ([#10192](https://github.com/parse-community/parse-server/issues/10192)) ([c7599c5](https://github.com/parse-community/parse-server/commit/c7599c577a02b97eb5e76d4e20517b0283ae73c8))
|
||||
|
||||
## [8.6.40](https://github.com/parse-community/parse-server/compare/8.6.39...8.6.40) (2026-03-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* GraphQL WebSocket endpoint bypasses security middleware ([GHSA-p2x3-8689-cwpg](https://github.com/parse-community/parse-server/security/advisories/GHSA-p2x3-8689-cwpg)) ([#10190](https://github.com/parse-community/parse-server/issues/10190)) ([21330d1](https://github.com/parse-community/parse-server/commit/21330d146c68b57a930a58b8a8cd9fbf09436cf3))
|
||||
|
||||
## [8.6.39](https://github.com/parse-community/parse-server/compare/8.6.38...8.6.39) (2026-03-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* OAuth2 adapter app ID validation sends wrong token to introspection endpoint ([GHSA-69xg-f649-w5g2](https://github.com/parse-community/parse-server/security/advisories/GHSA-69xg-f649-w5g2)) ([#10188](https://github.com/parse-community/parse-server/issues/10188)) ([fd6f6a6](https://github.com/parse-community/parse-server/commit/fd6f6a6ea9df631a63702d24496046ecccc610d6))
|
||||
|
||||
## [8.6.38](https://github.com/parse-community/parse-server/compare/8.6.37...8.6.38) (2026-03-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Account takeover via operator injection in authentication data identifier ([GHSA-5fw2-8jcv-xh87](https://github.com/parse-community/parse-server/security/advisories/GHSA-5fw2-8jcv-xh87)) ([#10186](https://github.com/parse-community/parse-server/issues/10186)) ([93425df](https://github.com/parse-community/parse-server/commit/93425df2bc9368eab89644c93fa9ef481c043e3a))
|
||||
|
||||
## [8.6.37](https://github.com/parse-community/parse-server/compare/8.6.36...8.6.37) (2026-03-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* OAuth2 adapter shares mutable state across providers via singleton instance ([GHSA-2cjm-2gwv-m892](https://github.com/parse-community/parse-server/security/advisories/GHSA-2cjm-2gwv-m892)) ([#10184](https://github.com/parse-community/parse-server/issues/10184)) ([6afa431](https://github.com/parse-community/parse-server/commit/6afa4315ea691d8fe36ed39f00fb50fd8affb691))
|
||||
|
||||
## [8.6.36](https://github.com/parse-community/parse-server/compare/8.6.35...8.6.36) (2026-03-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* SQL injection via query field name when using PostgreSQL ([GHSA-c442-97qw-j6c6](https://github.com/parse-community/parse-server/security/advisories/GHSA-c442-97qw-j6c6)) ([#10182](https://github.com/parse-community/parse-server/issues/10182)) ([0b0398b](https://github.com/parse-community/parse-server/commit/0b0398bd23cb243c59c13c94866454668064c013))
|
||||
|
||||
## [8.6.35](https://github.com/parse-community/parse-server/compare/8.6.34...8.6.35) (2026-03-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Protected fields bypass via LiveQuery subscription WHERE clause ([GHSA-j7mm-f4rv-6q6q](https://github.com/parse-community/parse-server/security/advisories/GHSA-j7mm-f4rv-6q6q)) ([#10176](https://github.com/parse-community/parse-server/issues/10176)) ([dfc7e69](https://github.com/parse-community/parse-server/commit/dfc7e69b95c719589d267f50935d8660e2201a8c))
|
||||
|
||||
## [8.6.34](https://github.com/parse-community/parse-server/compare/8.6.33...8.6.34) (2026-03-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* User enumeration via email verification endpoint ([GHSA-w54v-hf9p-8856](https://github.com/parse-community/parse-server/security/advisories/GHSA-w54v-hf9p-8856)) ([#10173](https://github.com/parse-community/parse-server/issues/10173)) ([d3defb8](https://github.com/parse-community/parse-server/commit/d3defb887d802aaef12600a1f0c9b729ea06eff9))
|
||||
|
||||
## [8.6.33](https://github.com/parse-community/parse-server/compare/8.6.32...8.6.33) (2026-03-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* MFA recovery codes not consumed after use ([GHSA-4hf6-3x24-c9m8](https://github.com/parse-community/parse-server/security/advisories/GHSA-4hf6-3x24-c9m8)) ([#10171](https://github.com/parse-community/parse-server/issues/10171)) ([a00c4fa](https://github.com/parse-community/parse-server/commit/a00c4fa24ff059081d2617dd435f8ee3de215000))
|
||||
|
||||
## [8.6.32](https://github.com/parse-community/parse-server/compare/8.6.31...8.6.32) (2026-03-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Protected fields bypass via dot-notation in query and sort ([GHSA-r2m8-pxm9-9c4g](https://github.com/parse-community/parse-server/security/advisories/GHSA-r2m8-pxm9-9c4g)) ([#10168](https://github.com/parse-community/parse-server/issues/10168)) ([1787db3](https://github.com/parse-community/parse-server/commit/1787db3244acca5ced180eb9814e1cfad364d826))
|
||||
|
||||
## [8.6.31](https://github.com/parse-community/parse-server/compare/8.6.30...8.6.31) (2026-03-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* SQL Injection via dot-notation sub-key name in `Increment` operation on PostgreSQL ([GHSA-gqpp-xgvh-9h7h](https://github.com/parse-community/parse-server/security/advisories/GHSA-gqpp-xgvh-9h7h)) ([#10166](https://github.com/parse-community/parse-server/issues/10166)) ([aa0de68](https://github.com/parse-community/parse-server/commit/aa0de68d20a23338c70db54f6c54f6028d263de1))
|
||||
|
||||
## [8.6.30](https://github.com/parse-community/parse-server/compare/8.6.29...8.6.30) (2026-03-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Stored XSS via file upload of HTML-renderable file types ([GHSA-v5hf-f4c3-m5rv](https://github.com/parse-community/parse-server/security/advisories/GHSA-v5hf-f4c3-m5rv)) ([#10164](https://github.com/parse-community/parse-server/issues/10164)) ([90936f9](https://github.com/parse-community/parse-server/commit/90936f9ca2d6d4a886b5549a8cdfabda98fcc168))
|
||||
|
||||
## [8.6.29](https://github.com/parse-community/parse-server/compare/8.6.28...8.6.29) (2026-03-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* SQL injection via `Increment` operation on nested object field in PostgreSQL ([GHSA-q3vj-96h2-gwvg](https://github.com/parse-community/parse-server/security/advisories/GHSA-q3vj-96h2-gwvg)) ([#10163](https://github.com/parse-community/parse-server/issues/10163)) ([c92022f](https://github.com/parse-community/parse-server/commit/c92022f1ff12b119d7a6a807426925ce7d52e5ab))
|
||||
|
||||
## [8.6.28](https://github.com/parse-community/parse-server/compare/8.6.27...8.6.28) (2026-03-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* SQL injection via dot-notation field name in PostgreSQL ([GHSA-qpr4-jrj4-6f27](https://github.com/parse-community/parse-server/security/advisories/GHSA-qpr4-jrj4-6f27)) ([#10160](https://github.com/parse-community/parse-server/issues/10160)) ([83f38fa](https://github.com/parse-community/parse-server/commit/83f38faab25d89e7bbe7c5c2087c5ee616479975))
|
||||
|
||||
## [8.6.27](https://github.com/parse-community/parse-server/compare/8.6.26...8.6.27) (2026-03-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* LiveQuery `regexTimeout` default value not applied ([#10157](https://github.com/parse-community/parse-server/issues/10157)) ([94c4f52](https://github.com/parse-community/parse-server/commit/94c4f523e02be6d82f006cbacb18bec5a1de7f2e))
|
||||
|
||||
## [8.6.26](https://github.com/parse-community/parse-server/compare/8.6.25...8.6.26) (2026-03-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* LDAP injection via unsanitized user input in DN and group filter construction ([GHSA-7m6r-fhh7-r47c](https://github.com/parse-community/parse-server/security/advisories/GHSA-7m6r-fhh7-r47c)) ([#10153](https://github.com/parse-community/parse-server/issues/10153)) ([2370611](https://github.com/parse-community/parse-server/commit/23706117220a7489558683f72e8fdc0983cf8dfc))
|
||||
|
||||
## [8.6.25](https://github.com/parse-community/parse-server/compare/8.6.24...8.6.25) (2026-03-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Classes `_GraphQLConfig` and `_Audience` master key bypass via generic class routes ([GHSA-7xg7-rqf6-pw6c](https://github.com/parse-community/parse-server/security/advisories/GHSA-7xg7-rqf6-pw6c)) ([#10152](https://github.com/parse-community/parse-server/issues/10152)) ([94aa653](https://github.com/parse-community/parse-server/commit/94aa65318c6a4b8d7b8f0b98c96d52e36d33dc9a))
|
||||
|
||||
## [8.6.24](https://github.com/parse-community/parse-server/compare/8.6.23...8.6.24) (2026-03-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Concurrent signup with same authentication creates duplicate users ([#10150](https://github.com/parse-community/parse-server/issues/10150)) ([fac8f33](https://github.com/parse-community/parse-server/commit/fac8f338ecdfeffcaed21d7e0729e2cf1ea9947d))
|
||||
|
||||
## [8.6.23](https://github.com/parse-community/parse-server/compare/8.6.22...8.6.23) (2026-03-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Rate limit bypass via batch request endpoint ([GHSA-775h-3xrc-c228](https://github.com/parse-community/parse-server/security/advisories/GHSA-775h-3xrc-c228)) ([#10148](https://github.com/parse-community/parse-server/issues/10148)) ([48b94ae](https://github.com/parse-community/parse-server/commit/48b94aed12006b5f1e501c0de8284a9541e60b1b))
|
||||
|
||||
## [8.6.22](https://github.com/parse-community/parse-server/compare/8.6.21...8.6.22) (2026-03-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Parse Server OAuth2 authentication adapter account takeover via identity spoofing ([GHSA-fr88-w35c-r596](https://github.com/parse-community/parse-server/security/advisories/GHSA-fr88-w35c-r596)) ([#10146](https://github.com/parse-community/parse-server/issues/10146)) ([238110b](https://github.com/parse-community/parse-server/commit/238110b5f63f47d9ef128bbd5d37795a85f31891))
|
||||
|
||||
## [8.6.21](https://github.com/parse-community/parse-server/compare/8.6.20...8.6.21) (2026-03-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Parse Server session token exfiltration via `redirectClassNameForKey` query parameter ([GHSA-6r2j-cxgf-495f](https://github.com/parse-community/parse-server/security/advisories/GHSA-6r2j-cxgf-495f)) ([#10144](https://github.com/parse-community/parse-server/issues/10144)) ([721abe8](https://github.com/parse-community/parse-server/commit/721abe8b7c76a3143936936a720d3782547d4d9c))
|
||||
|
||||
## [8.6.20](https://github.com/parse-community/parse-server/compare/8.6.19...8.6.20) (2026-03-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Parse Server role escalation and CLP bypass via direct `_Join table write ([GHSA-5f92-jrq3-28rc](https://github.com/parse-community/parse-server/security/advisories/GHSA-5f92-jrq3-28rc)) ([#10142](https://github.com/parse-community/parse-server/issues/10142)) ([1c58ef7](https://github.com/parse-community/parse-server/commit/1c58ef787894dcb67623c0fb889820ee1d2ddf1b))
|
||||
|
||||
## [8.6.19](https://github.com/parse-community/parse-server/compare/8.6.18...8.6.19) (2026-03-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Protected fields bypass via logical query operators ([GHSA-72hp-qff8-4pvv](https://github.com/parse-community/parse-server/security/advisories/GHSA-72hp-qff8-4pvv)) ([#10139](https://github.com/parse-community/parse-server/issues/10139)) ([2c11c61](https://github.com/parse-community/parse-server/commit/2c11c616cb911ceed322c8d4dd204de9a0abc323))
|
||||
|
||||
## [8.6.18](https://github.com/parse-community/parse-server/compare/8.6.17...8.6.18) (2026-03-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Missing audience validation in Keycloak authentication adapter ([GHSA-48mh-j4p5-7j9v](https://github.com/parse-community/parse-server/security/advisories/GHSA-48mh-j4p5-7j9v)) ([#10138](https://github.com/parse-community/parse-server/issues/10138)) ([572be64](https://github.com/parse-community/parse-server/commit/572be64d17935870a94840fe541ae91cc09143c2))
|
||||
|
||||
## [8.6.17](https://github.com/parse-community/parse-server/compare/8.6.16...8.6.17) (2026-03-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Stored cross-site scripting (XSS) via SVG file upload ([GHSA-hcj7-6gxh-24ww](https://github.com/parse-community/parse-server/security/advisories/GHSA-hcj7-6gxh-24ww)) ([#10135](https://github.com/parse-community/parse-server/issues/10135)) ([fed2d39](https://github.com/parse-community/parse-server/commit/fed2d39f1dc4a64d093e77d54ebc7f6963918d15))
|
||||
|
||||
## [8.6.16](https://github.com/parse-community/parse-server/compare/8.6.15...8.6.16) (2026-03-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Bypass of class-level permissions in LiveQuery ([GHSA-7ch5-98q2-7289](https://github.com/parse-community/parse-server/security/advisories/GHSA-7ch5-98q2-7289)) ([#10134](https://github.com/parse-community/parse-server/issues/10134)) ([6fecec5](https://github.com/parse-community/parse-server/commit/6fecec52c53f24baefbfd5b5899bf295069447d6))
|
||||
|
||||
## [8.6.15](https://github.com/parse-community/parse-server/compare/8.6.14...8.6.15) (2026-03-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Denial-of-service via unbounded query complexity in REST and GraphQL API ([GHSA-cmj3-wx7h-ffvg](https://github.com/parse-community/parse-server/security/advisories/GHSA-cmj3-wx7h-ffvg)) ([#10131](https://github.com/parse-community/parse-server/issues/10131)) ([23ac059](https://github.com/parse-community/parse-server/commit/23ac05938b64451322b60fe6b031b4893ff62b67))
|
||||
|
||||
## [8.6.14](https://github.com/parse-community/parse-server/compare/8.6.13...8.6.14) (2026-03-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* NoSQL injection via token type in password reset and email verification endpoints ([GHSA-vgjh-hmwf-c588](https://github.com/parse-community/parse-server/security/advisories/GHSA-vgjh-hmwf-c588)) ([#10129](https://github.com/parse-community/parse-server/issues/10129)) ([88eed83](https://github.com/parse-community/parse-server/commit/88eed83ff818027a960274e1de30a487812a6db4))
|
||||
|
||||
## [8.6.13](https://github.com/parse-community/parse-server/compare/8.6.12...8.6.13) (2026-03-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Denial of Service (DoS) and Cloud Function Dispatch Bypass via Prototype Chain Resolution ([GHSA-5j86-7r7m-p8h6](https://github.com/parse-community/parse-server/security/advisories/GHSA-5j86-7r7m-p8h6)) ([#10124](https://github.com/parse-community/parse-server/issues/10124)) ([5c2d60a](https://github.com/parse-community/parse-server/commit/5c2d60a2f3733ca3a4cb782d552ba38c526aee0b))
|
||||
|
||||
## [8.6.12](https://github.com/parse-community/parse-server/compare/8.6.11...8.6.12) (2026-03-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Denylist `requestKeywordDenylist` keyword scan bypass through nested object placement ([GHSA-q342-9w2p-57fp](https://github.com/parse-community/parse-server/security/advisories/GHSA-q342-9w2p-57fp)) ([#10122](https://github.com/parse-community/parse-server/issues/10122)) ([2b52feb](https://github.com/parse-community/parse-server/commit/2b52feb06448e5c683017fa2e3d2038a5d8d6085))
|
||||
|
||||
## [8.6.11](https://github.com/parse-community/parse-server/compare/8.6.10...8.6.11) (2026-03-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Regular Expression Denial of Service (ReDoS) via `$regex` query in LiveQuery ([GHSA-mf3j-86qx-cq5j](https://github.com/parse-community/parse-server/security/advisories/GHSA-mf3j-86qx-cq5j)) ([#10120](https://github.com/parse-community/parse-server/issues/10120)) ([42bd2f0](https://github.com/parse-community/parse-server/commit/42bd2f07bd3b425cac1e3c48161688cc4d54ef41))
|
||||
|
||||
## [8.6.10](https://github.com/parse-community/parse-server/compare/8.6.9...8.6.10) (2026-03-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* JWT audience validation bypass in Google, Apple, and Facebook authentication adapters ([GHSA-x6fw-778m-wr9v](https://github.com/parse-community/parse-server/security/advisories/GHSA-x6fw-778m-wr9v)) ([#10114](https://github.com/parse-community/parse-server/issues/10114)) ([1da3123](https://github.com/parse-community/parse-server/commit/1da312311827a7790ad97852e8672119d40d529a))
|
||||
|
||||
## [8.6.9](https://github.com/parse-community/parse-server/compare/8.6.8...8.6.9) (2026-03-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* File metadata endpoint bypasses `beforeFind` / `afterFind` trigger authorization ([GHSA-hwx8-q9cg-mqmc](https://github.com/parse-community/parse-server/security/advisories/GHSA-hwx8-q9cg-mqmc)) ([#10107](https://github.com/parse-community/parse-server/issues/10107)) ([a7358b1](https://github.com/parse-community/parse-server/commit/a7358b1e0c58ef4c6b5e0ade772bf673b5f78fd0))
|
||||
|
||||
## [8.6.8](https://github.com/parse-community/parse-server/compare/8.6.7...8.6.8) (2026-03-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `PagesRouter` path traversal allows reading files outside configured pages directory ([GHSA-hm3f-q6rw-m6wh](https://github.com/parse-community/parse-server/security/advisories/GHSA-hm3f-q6rw-m6wh)) ([#10105](https://github.com/parse-community/parse-server/issues/10105)) ([d5a057d](https://github.com/parse-community/parse-server/commit/d5a057d1a7cd5f6713d93afa3ad6f764f74b6ed2))
|
||||
|
||||
## [8.6.7](https://github.com/parse-community/parse-server/compare/8.6.6...8.6.7) (2026-03-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Malformed `$regex` query leaks database error details in API response (GHSA-9cp7-3q5w-j92g) ([#10102](https://github.com/parse-community/parse-server/issues/10102)) ([07870f5](https://github.com/parse-community/parse-server/commit/07870f59eec03f5c2a5fb1732cb28787ca3f8152))
|
||||
|
||||
## [8.6.6](https://github.com/parse-community/parse-server/compare/8.6.5...8.6.6) (2026-03-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Endpoint `/loginAs` allows `readOnlyMasterKey` to gain full read and write access as any user ([GHSA-79wj-8rqv-jvp5](https://github.com/parse-community/parse-server/security/advisories/GHSA-79wj-8rqv-jvp5)) ([#10099](https://github.com/parse-community/parse-server/issues/10099)) ([0c940b7](https://github.com/parse-community/parse-server/commit/0c940b70891c947fbf6c55536ed95ae300c23350))
|
||||
|
||||
## [8.6.5](https://github.com/parse-community/parse-server/compare/8.6.4...8.6.5) (2026-03-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* File creation and deletion bypasses `readOnlyMasterKey` write restriction (GHSA-xfh7-phr7-gr2x) ([#10096](https://github.com/parse-community/parse-server/issues/10096)) ([07bddc0](https://github.com/parse-community/parse-server/commit/07bddc0850c0eebb51219fe1d5d342f4412461ba))
|
||||
|
||||
## [8.6.4](https://github.com/parse-community/parse-server/compare/8.6.3...8.6.4) (2026-03-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Cloud Hooks and Cloud Jobs bypass `readOnlyMasterKey` write restriction (GHSA-vc89-5g3r-cmhh) ([#10089](https://github.com/parse-community/parse-server/issues/10089)) ([6c79da9](https://github.com/parse-community/parse-server/commit/6c79da91fc5ec6f2a0bb69a0ca6a886c1585754f))
|
||||
|
||||
## [8.6.3](https://github.com/parse-community/parse-server/compare/8.6.2...8.6.3) (2026-02-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* JWT Algorithm Confusion in Google Auth Adapter ([GHSA-4q3h-vp4r-prv2](https://github.com/parse-community/parse-server/security/advisories/GHSA-4q3h-vp4r-prv2)) ([#10073](https://github.com/parse-community/parse-server/issues/10073)) ([9b94083](https://github.com/parse-community/parse-server/commit/9b94083accb7f3e72c6b8126c195c7a03dd2dfd7))
|
||||
|
||||
## [8.6.2](https://github.com/parse-community/parse-server/compare/8.6.1...8.6.2) (2025-12-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Server-Side Request Forgery (SSRF) in Instagram auth adapter [GHSA-3f5f-xgrj-97pf](https://github.com/parse-community/parse-server/security/advisories/GHSA-3f5f-xgrj-97pf) ([#9989](https://github.com/parse-community/parse-server/issues/9989)) ([155c6ad](https://github.com/parse-community/parse-server/commit/155c6ad92d2375652c9720d7deed129a9e8f74ff))
|
||||
|
||||
## [8.6.1](https://github.com/parse-community/parse-server/compare/8.6.0...8.6.1) (2025-12-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Cross-Site Scripting (XSS) via HTML pages for password reset and email verification [GHSA-jhgf-2h8h-ggxv](https://github.com/parse-community/parse-server/security/advisories/GHSA-jhgf-2h8h-ggxv) ([#9986](https://github.com/parse-community/parse-server/issues/9986)) ([12d8b50](https://github.com/parse-community/parse-server/commit/12d8b502a2f99098d177d095842b07d55f62313a))
|
||||
|
||||
# [8.6.0](https://github.com/parse-community/parse-server/compare/8.5.0...8.6.0) (2025-12-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Remove elevated permissions in GitHub CI performance benchmark ([#9966](https://github.com/parse-community/parse-server/issues/9966)) ([6b9f896](https://github.com/parse-community/parse-server/commit/6b9f8963cc3debf59cd9c5dfc5422aff9404ce9d))
|
||||
|
||||
### Features
|
||||
|
||||
* Add GraphQL query `cloudConfig` to retrieve and mutation `updateCloudConfig` to update Cloud Config ([#9947](https://github.com/parse-community/parse-server/issues/9947)) ([3ca85cd](https://github.com/parse-community/parse-server/commit/3ca85cd4a632f234c9d3d731331c0524dfe54075))
|
||||
|
||||
# [8.5.0](https://github.com/parse-community/parse-server/compare/8.4.0...8.5.0) (2025-12-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `GridFSBucketAdapter` throws when using some Parse Server specific options in MongoDB database options ([#9915](https://github.com/parse-community/parse-server/issues/9915)) ([d3d4003](https://github.com/parse-community/parse-server/commit/d3d4003570b9872f2b0f5a25fc06ce4c4132860d))
|
||||
* Deprecation warning logged at server launch for nested Parse Server option even if option is explicitly set ([#9934](https://github.com/parse-community/parse-server/issues/9934)) ([c22cb0a](https://github.com/parse-community/parse-server/commit/c22cb0ae58e64cd0e4597ab9610d57a1155c44a2))
|
||||
* Parse Server option `rateLimit.zone` does not use default value `ip` ([#9941](https://github.com/parse-community/parse-server/issues/9941)) ([12beb8f](https://github.com/parse-community/parse-server/commit/12beb8f6ee5d3002fec017bb4525eb3f1375f806))
|
||||
* Queries with object field `authData.provider.id` are incorrectly transformed to `_auth_data_provider.id` for custom classes ([#9932](https://github.com/parse-community/parse-server/issues/9932)) ([7b9fa18](https://github.com/parse-community/parse-server/commit/7b9fa18f968ec084ea0b35dad2b5ba0451d59787))
|
||||
* Race condition can cause multiple Apollo server initializations under load ([#9929](https://github.com/parse-community/parse-server/issues/9929)) ([7d5e9fc](https://github.com/parse-community/parse-server/commit/7d5e9fcf3ceb0abad8ab49c75bc26f521a0f1bde))
|
||||
* Server internal error details leaking in error messages returned to clients ([#9937](https://github.com/parse-community/parse-server/issues/9937)) ([50edb5a](https://github.com/parse-community/parse-server/commit/50edb5ab4bb4a6ce474bfb7cf159d918933753b8))
|
||||
|
||||
### Features
|
||||
|
||||
* Add `beforePasswordResetRequest` hook ([#9906](https://github.com/parse-community/parse-server/issues/9906)) ([94cee5b](https://github.com/parse-community/parse-server/commit/94cee5bfafca10c914c73cf17fcdb627a9f0837b))
|
||||
* Add MongoDB client event logging via database option `logClientEvents` ([#9914](https://github.com/parse-community/parse-server/issues/9914)) ([b760733](https://github.com/parse-community/parse-server/commit/b760733b98bcfc9c09ac9780066602e1fda108fe))
|
||||
* Add Parse Server option `allowPublicExplain` to allow `Parse.Query.explain` without master key ([#9890](https://github.com/parse-community/parse-server/issues/9890)) ([4456b02](https://github.com/parse-community/parse-server/commit/4456b02280c2d8dd58b7250e9e67f1a8647b3452))
|
||||
* Add Parse Server option `enableSanitizedErrorResponse` to remove detailed error messages from responses sent to clients ([#9944](https://github.com/parse-community/parse-server/issues/9944)) ([4752197](https://github.com/parse-community/parse-server/commit/47521974aeafcf41102be62f19612a4ab0a4837f))
|
||||
* Add support for MongoDB driver options `serverSelectionTimeoutMS`, `maxIdleTimeMS`, `heartbeatFrequencyMS` ([#9910](https://github.com/parse-community/parse-server/issues/9910)) ([1b661e9](https://github.com/parse-community/parse-server/commit/1b661e98c86a1db79e076a7297cd9199a72ae1ac))
|
||||
* Add support for more MongoDB driver options ([#9911](https://github.com/parse-community/parse-server/issues/9911)) ([cff451e](https://github.com/parse-community/parse-server/commit/cff451eabdc380affa600ed711de66f7bd1d00aa))
|
||||
* Allow option `publicServerURL` to be set dynamically as asynchronous function ([#9803](https://github.com/parse-community/parse-server/issues/9803)) ([460a65c](https://github.com/parse-community/parse-server/commit/460a65cf612f4c86af8038cafcc7e7ffe9eb8440))
|
||||
* Upgrade to parse 7.1.1 ([#9954](https://github.com/parse-community/parse-server/issues/9954)) ([fa57d69](https://github.com/parse-community/parse-server/commit/fa57d69cbec525189da98d7136c1c0e9eaf74338))
|
||||
* Upgrade to parse 7.1.2 ([#9955](https://github.com/parse-community/parse-server/issues/9955)) ([5c644a5](https://github.com/parse-community/parse-server/commit/5c644a55ac25986f214b68ba4bcbe7a62ad6d6d1))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* `Parse.Query.include` now fetches pointers at same level in parallel ([#9861](https://github.com/parse-community/parse-server/issues/9861)) ([dafea21](https://github.com/parse-community/parse-server/commit/dafea21eb39b0fdc2b52bb8a14f7b61e3f2b8d13))
|
||||
* Remove unused dependencies ([#9943](https://github.com/parse-community/parse-server/issues/9943)) ([d4c6de0](https://github.com/parse-community/parse-server/commit/d4c6de0096b3ac95289c6bddfe25eb397d790e41))
|
||||
* Upgrade MongoDB driver to 6.20.0 ([#9887](https://github.com/parse-community/parse-server/issues/9887)) ([3c9af48](https://github.com/parse-community/parse-server/commit/3c9af48edd999158443b797e388e29495953799e))
|
||||
|
||||
# [8.4.0](https://github.com/parse-community/parse-server/compare/8.3.0...8.4.0) (2025-11-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Add problematic MIME types to default value of Parse Server option `fileUpload.fileExtensions` ([#9902](https://github.com/parse-community/parse-server/issues/9902)) ([fa245cb](https://github.com/parse-community/parse-server/commit/fa245cbb5f5b7a0dad962b2ce0524fa4dafcb5f7))
|
||||
* Uploading a file by providing an origin URL allows for Server-Side Request Forgery (SSRF); fixes vulnerability [GHSA-x4qj-2f4q-r4rx](https://github.com/parse-community/parse-server/security/advisories/GHSA-x4qj-2f4q-r4rx) ([#9903](https://github.com/parse-community/parse-server/issues/9903)) ([9776386](https://github.com/parse-community/parse-server/commit/97763863b72689a29ad7a311dfb590c3e3c50585))
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for Node 24 ([#9901](https://github.com/parse-community/parse-server/issues/9901)) ([25dfe19](https://github.com/parse-community/parse-server/commit/25dfe19fef02fd44224e4a6d198584a694a1aa52))
|
||||
|
||||
# [8.3.0](https://github.com/parse-community/parse-server/compare/8.2.5...8.3.0) (2025-11-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Error in `afterSave` trigger for `Parse.Role` due to `name` field ([#9883](https://github.com/parse-community/parse-server/issues/9883)) ([eb052d8](https://github.com/parse-community/parse-server/commit/eb052d8e6abe1ae32505fd068d5445eaf950a770))
|
||||
* Indexes `_email_verify_token` for email verification and `_perishable_token` password reset are not created automatically ([#9893](https://github.com/parse-community/parse-server/issues/9893)) ([62dd3c5](https://github.com/parse-community/parse-server/commit/62dd3c565ab70765cb1c547996b616b72e9bb800))
|
||||
* Security upgrade to parse 7.0.1 ([#9877](https://github.com/parse-community/parse-server/issues/9877)) ([abfa94c](https://github.com/parse-community/parse-server/commit/abfa94cd6de2c4e76337931c8ea8311c4ccf2a1a))
|
||||
* Server URL verification before server is ready ([#9882](https://github.com/parse-community/parse-server/issues/9882)) ([178bd5c](https://github.com/parse-community/parse-server/commit/178bd5c5e258d9501c9ac4d35a3a105ab64be67e))
|
||||
* Stale data read in validation query on `Parse.Object` update causes inconsistency between validation read and subsequent update write operation ([#9859](https://github.com/parse-community/parse-server/issues/9859)) ([f49efaf](https://github.com/parse-community/parse-server/commit/f49efaf5bb1d6b19f6d6712f7cdf073855c95c6e))
|
||||
* Warning logged when setting option `databaseOptions.disableIndexFieldValidation` ([#9880](https://github.com/parse-community/parse-server/issues/9880)) ([1815b01](https://github.com/parse-community/parse-server/commit/1815b019b52565d2bc87be2596a49aea7600aeba))
|
||||
|
||||
### Features
|
||||
|
||||
* Add option `keepUnknownIndexes` to retain indexes which are not specified in schema ([#9857](https://github.com/parse-community/parse-server/issues/9857)) ([89fad46](https://github.com/parse-community/parse-server/commit/89fad468c3a43772879c06c4d939a83b72517a8e))
|
||||
* Add options to skip automatic creation of internal database indexes on server start ([#9897](https://github.com/parse-community/parse-server/issues/9897)) ([ea91aca](https://github.com/parse-community/parse-server/commit/ea91aca1420c33e038516a321b2640709589f886))
|
||||
* Add Parse Server option `verifyServerUrl` to disable server URL verification on server launch ([#9881](https://github.com/parse-community/parse-server/issues/9881)) ([b298ccc](https://github.com/parse-community/parse-server/commit/b298cccd9fb4f664b9d83894faad6d1ea7a3c964))
|
||||
* Add regex option `u` for unicode support in `Parse.Query.matches` for MongoDB ([#9867](https://github.com/parse-community/parse-server/issues/9867)) ([7cb962a](https://github.com/parse-community/parse-server/commit/7cb962a02845f3dded61baffd84515f94b66ee50))
|
||||
* Add request context middleware for config and dependency injection in hooks ([#8480](https://github.com/parse-community/parse-server/issues/8480)) ([64f104e](https://github.com/parse-community/parse-server/commit/64f104e5c5f8863098e801eee632c14fcbd9b6f9))
|
||||
* Add support for Postgres 18 ([#9870](https://github.com/parse-community/parse-server/issues/9870)) ([d275c18](https://github.com/parse-community/parse-server/commit/d275c1806e0a5a037cc06cde7eefff3e12c91d7d))
|
||||
* Allow returning objects in `Parse.Cloud.beforeFind` without invoking database query ([#9770](https://github.com/parse-community/parse-server/issues/9770)) ([0b47407](https://github.com/parse-community/parse-server/commit/0b4740714c29ba99672bc535619ee3516abd356f))
|
||||
* Disable index-field validation to create index for fields that don't yet exist ([#8137](https://github.com/parse-community/parse-server/issues/8137)) ([1b23475](https://github.com/parse-community/parse-server/commit/1b2347524ca84ade0f6badf175a815fc8a7bef49))
|
||||
|
||||
## [8.2.5](https://github.com/parse-community/parse-server/compare/8.2.4...8.2.5) (2025-10-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* GraphQL playground shows blank page ([#9858](https://github.com/parse-community/parse-server/issues/9858)) ([7b5395c](https://github.com/parse-community/parse-server/commit/7b5395c5d481235c022d96603280672366a50715))
|
||||
|
||||
## [8.2.4](https://github.com/parse-community/parse-server/compare/8.2.3...8.2.4) (2025-09-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Security upgrade form-data from 4.0.3 to 4.0.4 ([#9829](https://github.com/parse-community/parse-server/issues/9829)) ([c2c593f](https://github.com/parse-community/parse-server/commit/c2c593f437c33f37101b4f3bb1287eef31dbbc3b))
|
||||
|
||||
## [8.2.3](https://github.com/parse-community/parse-server/compare/8.2.2...8.2.3) (2025-08-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* MongoDB aggregation pipeline with `$dateSubtract` from `$$NOW` returns no results ([#9822](https://github.com/parse-community/parse-server/issues/9822)) ([847a274](https://github.com/parse-community/parse-server/commit/847a274cdb8c22f8e0fc249162e5e2c9e29a594a))
|
||||
|
||||
## [8.2.2](https://github.com/parse-community/parse-server/compare/8.2.1...8.2.2) (2025-07-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Data schema exposed via GraphQL API public introspection (GHSA-48q3-prgv-gm4w) ([#9819](https://github.com/parse-community/parse-server/issues/9819)) ([c58b2eb](https://github.com/parse-community/parse-server/commit/c58b2eb6eb48af9d3c2e69b4829810a021347b40))
|
||||
|
||||
## [8.2.1](https://github.com/parse-community/parse-server/compare/8.2.0...8.2.1) (2025-06-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `Parse.Query.containedIn` and `matchesQuery` do not work with nested objects ([#9738](https://github.com/parse-community/parse-server/issues/9738)) ([0db3a6f](https://github.com/parse-community/parse-server/commit/0db3a6ff27a129427770e314a792cc586e4255b5))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* Remove saving Parse Cloud Job request parameters in internal collection `_JobStatus` ([#8343](https://github.com/parse-community/parse-server/issues/8343)) ([e98733c](https://github.com/parse-community/parse-server/commit/e98733cbac9451521a3acc388d2f9d29eb4610e0))
|
||||
|
||||
# [8.2.0](https://github.com/parse-community/parse-server/compare/8.1.0...8.2.0) (2025-05-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add TypeScript definitions ([#9693](https://github.com/parse-community/parse-server/issues/9693)) ([e86718f](https://github.com/parse-community/parse-server/commit/e86718fc59c7c8e6f3c6abd0feb7d1a68ca76c23))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* Add details to error message in `Parse.Query.aggregate` ([#9689](https://github.com/parse-community/parse-server/issues/9689)) ([9de6999](https://github.com/parse-community/parse-server/commit/9de6999e257d839b68bbca282447777edfdb1ddf))
|
||||
|
||||
# [8.1.0](https://github.com/parse-community/parse-server/compare/8.0.2...8.1.0) (2025-04-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Parse Server doesn't shutdown gracefully ([#9634](https://github.com/parse-community/parse-server/issues/9634)) ([aed918d](https://github.com/parse-community/parse-server/commit/aed918d3109e739f7231d481b5f48c68fc01cf04))
|
||||
|
||||
### Features
|
||||
|
||||
* Add Cloud Code triggers `Parse.Cloud.beforeFind(Parse.File)`and `Parse.Cloud.afterFind(Parse.File)` ([#8700](https://github.com/parse-community/parse-server/issues/8700)) ([b2beaa8](https://github.com/parse-community/parse-server/commit/b2beaa86ff543a7aa4ad274c7a23bc4aa302c3fa))
|
||||
* Add default ACL ([#8701](https://github.com/parse-community/parse-server/issues/8701)) ([12b5d78](https://github.com/parse-community/parse-server/commit/12b5d781dc3f8c43c0c566dffa9308d02a7d8043))
|
||||
* Upgrade Parse JS SDK from 6.0.0 to 6.1.0 ([#9686](https://github.com/parse-community/parse-server/issues/9686)) ([f49c371](https://github.com/parse-community/parse-server/commit/f49c371c1373d41e68b091e65f33a71ff6fc6dd0))
|
||||
|
||||
## [8.0.2](https://github.com/parse-community/parse-server/compare/8.0.1...8.0.2) (2025-03-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Authentication provider credentials are usable across Parse Server apps; fixes security vulnerability [GHSA-837q-jhwx-cmpv](https://github.com/parse-community/parse-server/security/advisories/GHSA-837q-jhwx-cmpv) ([#9667](https://github.com/parse-community/parse-server/issues/9667)) ([5ef0440](https://github.com/parse-community/parse-server/commit/5ef0440c8e763854e62341acaeb6dc4ade3ba82f))
|
||||
|
||||
## [8.0.1](https://github.com/parse-community/parse-server/compare/8.0.0...8.0.1) (2025-03-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Security upgrade node from 20.18.2-alpine3.20 to 20.19.0-alpine3.20 ([#9652](https://github.com/parse-community/parse-server/issues/9652)) ([2be1a19](https://github.com/parse-community/parse-server/commit/2be1a19a13d6f0f8e3eb4e399a6279ff4d01db76))
|
||||
* Using Parse Server option `extendSessionOnUse` does not correctly clear memory and functions as a debounce instead of a throttle ([#8683](https://github.com/parse-community/parse-server/issues/8683)) ([6258a6a](https://github.com/parse-community/parse-server/commit/6258a6a11235dc642c71074d24e19c055294d26d))
|
||||
|
||||
# [8.0.0](https://github.com/parse-community/parse-server/compare/7.4.0...8.0.0) (2025-03-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* LiveQueryServer crashes using cacheAdapter on disconnect from Redis 4 server ([#9616](https://github.com/parse-community/parse-server/issues/9616)) ([bbc6bd4](https://github.com/parse-community/parse-server/commit/bbc6bd4b3f493170c13ad3314924cbf1f379eca4))
|
||||
* Push adapter not loading on some versions of Node 22 ([#9524](https://github.com/parse-community/parse-server/issues/9524)) ([ff7f671](https://github.com/parse-community/parse-server/commit/ff7f671c79f5dcdc44e4319a10f3654e12662c23))
|
||||
* Remove username from email verification and password reset process ([#8488](https://github.com/parse-community/parse-server/issues/8488)) ([d21dd97](https://github.com/parse-community/parse-server/commit/d21dd973363f9c5eca86a1007cb67e445b0d2e02))
|
||||
* Security upgrade node from 20.17.0-alpine3.20 to 20.18.2-alpine3.20 ([#9583](https://github.com/parse-community/parse-server/issues/9583)) ([8f85ae2](https://github.com/parse-community/parse-server/commit/8f85ae205474f65414c0536754de12c87dbbf82a))
|
||||
|
||||
### Features
|
||||
|
||||
* Add dynamic master key by setting Parse Server option `masterKey` to a function ([#9582](https://github.com/parse-community/parse-server/issues/9582)) ([6f1d161](https://github.com/parse-community/parse-server/commit/6f1d161a2f263a166981f9544cf2aadce65afe23))
|
||||
* Add support for MongoDB `databaseOptions` keys `autoSelectFamily`, `autoSelectFamilyAttemptTimeout` ([#9579](https://github.com/parse-community/parse-server/issues/9579)) ([5966068](https://github.com/parse-community/parse-server/commit/5966068e963e7a79eac8fba8720ee7d83578f207))
|
||||
* Add support for MongoDB `databaseOptions` keys `minPoolSize`, `connectTimeoutMS`, `socketTimeoutMS` ([#9522](https://github.com/parse-community/parse-server/issues/9522)) ([91618fe](https://github.com/parse-community/parse-server/commit/91618fe738217b937cbfcec35969679e0adb7676))
|
||||
* Add TypeScript support ([#9550](https://github.com/parse-community/parse-server/issues/9550)) ([59e46d0](https://github.com/parse-community/parse-server/commit/59e46d0aea3e6529994d98160d993144b8075291))
|
||||
* Change default value of Parse Server option `encodeParseObjectInCloudFunction` to `true` ([#9527](https://github.com/parse-community/parse-server/issues/9527)) ([5c5ad69](https://github.com/parse-community/parse-server/commit/5c5ad69b4a917b7ed7c328a8255144e105c40b08))
|
||||
* Deprecate `PublicAPIRouter` in favor of `PagesRouter` ([#9526](https://github.com/parse-community/parse-server/issues/9526)) ([7f66629](https://github.com/parse-community/parse-server/commit/7f666292e8b9692966672486b7108edefc356309))
|
||||
* Increase required minimum MongoDB versions to `6.0.19`, `7.0.16`, `8.0.4` ([#9531](https://github.com/parse-community/parse-server/issues/9531)) ([871e508](https://github.com/parse-community/parse-server/commit/871e5082a9fd768cee3012e26d3c8ddff5c2952c))
|
||||
* Increase required minimum Node versions to `18.20.4`, `20.18.0`, `22.12.0` ([#9521](https://github.com/parse-community/parse-server/issues/9521)) ([4e151cd](https://github.com/parse-community/parse-server/commit/4e151cd0a52191809452f197b2f29c3a12525b67))
|
||||
* Increase required minimum versions to Postgres `15`, PostGIS `3.3` ([#9538](https://github.com/parse-community/parse-server/issues/9538)) ([89c9b54](https://github.com/parse-community/parse-server/commit/89c9b5485a07a411fb35de4f8cf0467e7eb01f85))
|
||||
* Upgrade to express 5.0.1 ([#9530](https://github.com/parse-community/parse-server/issues/9530)) ([e0480df](https://github.com/parse-community/parse-server/commit/e0480dfa8d97946e57eac6b74d937978f8454b3a))
|
||||
* Upgrade to Parse JS SDK 6.0.0 ([#9624](https://github.com/parse-community/parse-server/issues/9624)) ([bf9db75](https://github.com/parse-community/parse-server/commit/bf9db75e8685def1407034944725e758bc926c26))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* This upgrades the internally used Express framework from version 4 to 5, which may be a breaking change. If Parse Server is set up to be mounted on an Express application, we recommend to also use version 5 of the Express framework to avoid any compatibility issues. Note that even if there are no issues after upgrading, future releases of Parse Server may introduce issues if Parse Server internally relies on Express 5-specific features which are unsupported by the Express version on which it is mounted. See the Express [migration guide](https://expressjs.com/en/guide/migrating-5.html) and [release announcement](https://expressjs.com/2024/10/15/v5-release.html#breaking-changes) for more info. ([e0480df](e0480df))
|
||||
* This upgrades to the Parse JS SDK 6.0.0. See the [change log](https://github.com/parse-community/Parse-SDK-JS/releases/tag/6.0.0) of the Parse JS SDK for breaking changes and more details. ([bf9db75](bf9db75))
|
||||
* This removes the username from the email verification and password reset process to prevent storing personally identifiable information (PII) in server and infrastructure logs. Customized HTML pages or emails related to email verification and password reset may need to be adapted accordingly. See the new templates that come bundled with Parse Server and the [migration guide](https://github.com/parse-community/parse-server/blob/alpha/8.0.0.md) for more details. ([d21dd97](d21dd97))
|
||||
* This releases increases the required minimum versions to Postgres `15`, PostGIS `3.3` and removes support for Postgres `13`, `14`, PostGIS `3.1`, `3.2`. ([89c9b54](89c9b54))
|
||||
* The default value of Parse Server option `encodeParseObjectInCloudFunction` changes to `true`; the option has been deprecated and will be removed in a future version. ([5c5ad69](5c5ad69))
|
||||
* This releases increases the required minimum MongoDB versions to `6.0.19`, `7.0.16`, `8.0.4` and removes support for MongoDB `4`, `5`. ([871e508](871e508))
|
||||
* This releases increases the required minimum Node versions to 18.20.4, 20.18.0, 22.12.0 and removes unofficial support for Node 19. ([4e151cd](4e151cd))
|
||||
|
||||
# [7.4.0](https://github.com/parse-community/parse-server/compare/7.3.0...7.4.0) (2024-12-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `Parse.Query.distinct` fails due to invalid aggregate stage 'hint' ([#9295](https://github.com/parse-community/parse-server/issues/9295)) ([5f66c6a](https://github.com/parse-community/parse-server/commit/5f66c6a075cbe1cdaf9d1b108ee65af8ae596b89))
|
||||
* Security upgrade cross-spawn from 7.0.3 to 7.0.6 ([#9444](https://github.com/parse-community/parse-server/issues/9444)) ([3d034e0](https://github.com/parse-community/parse-server/commit/3d034e0a993e3e5bd9bb96a7e382bb3464f1eb68))
|
||||
* Security upgrade fast-xml-parser from 4.4.0 to 4.4.1 ([#9262](https://github.com/parse-community/parse-server/issues/9262)) ([992d39d](https://github.com/parse-community/parse-server/commit/992d39d508f230c774dcb764d1d907ec8887e6c5))
|
||||
* Security upgrade node from 20.14.0-alpine3.20 to 20.17.0-alpine3.20 ([#9300](https://github.com/parse-community/parse-server/issues/9300)) ([15bb17d](https://github.com/parse-community/parse-server/commit/15bb17d87153bf0d38f08fe4c720da29a204b36b))
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for MongoDB 8 ([#9269](https://github.com/parse-community/parse-server/issues/9269)) ([4756c66](https://github.com/parse-community/parse-server/commit/4756c66cd9f55afa1621d1a3f6fa850ed605cb53))
|
||||
* Add support for PostGIS 3.5 ([#9354](https://github.com/parse-community/parse-server/issues/9354)) ([8ea3538](https://github.com/parse-community/parse-server/commit/8ea35382db3436d54ab59bd30706705564b0985c))
|
||||
* Add support for Postgres 17 ([#9324](https://github.com/parse-community/parse-server/issues/9324)) ([fa2ee31](https://github.com/parse-community/parse-server/commit/fa2ee3196e4319a142b3838bb947c98dcba5d5cb))
|
||||
* Upgrade @parse/push-adapter from 6.7.1 to 6.8.0 ([#9489](https://github.com/parse-community/parse-server/issues/9489)) ([286aa66](https://github.com/parse-community/parse-server/commit/286aa664ac8830d36c3e70d2316917d15f0b6df5))
|
||||
|
||||
# [7.3.0](https://github.com/parse-community/parse-server/compare/7.2.0...7.3.0) (2024-10-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Custom object ID allows to acquire role privileges ([GHSA-8xq9-g7ch-35hg](https://github.com/parse-community/parse-server/security/advisories/GHSA-8xq9-g7ch-35hg)) ([#9317](https://github.com/parse-community/parse-server/issues/9317)) ([13ee52f](https://github.com/parse-community/parse-server/commit/13ee52f0d19ef3a3524b3d79aea100e587eb3cfc))
|
||||
* Parse Server `databaseOptions` nested keys incorrectly identified as invalid ([#9213](https://github.com/parse-community/parse-server/issues/9213)) ([77206d8](https://github.com/parse-community/parse-server/commit/77206d804443cfc1618c24f8961bd677de9920c0))
|
||||
* Parse Server installation fails due to post install script incorrectly parsing required min. Node version ([#9216](https://github.com/parse-community/parse-server/issues/9216)) ([0fa82a5](https://github.com/parse-community/parse-server/commit/0fa82a54fe38ec14e8054339285d3db71a8624c8))
|
||||
* Parse Server option `maxLogFiles` doesn't recognize day duration literals such as `1d` to mean 1 day ([#9215](https://github.com/parse-community/parse-server/issues/9215)) ([0319cee](https://github.com/parse-community/parse-server/commit/0319cee2dbf65e90bad377af1ed14ea25c595bf5))
|
||||
* Security upgrade path-to-regexp from 6.2.1 to 6.3.0 ([#9314](https://github.com/parse-community/parse-server/issues/9314)) ([8b7fe69](https://github.com/parse-community/parse-server/commit/8b7fe699c1c376ecd8cc1c97cce8e704ee41f28a))
|
||||
|
||||
### Features
|
||||
|
||||
* Add atomic operations for Cloud Config parameters ([#9219](https://github.com/parse-community/parse-server/issues/9219)) ([35cadf9](https://github.com/parse-community/parse-server/commit/35cadf9b8324879fb7309ba5d7ea46f2c722d614))
|
||||
* Add Cloud Code triggers `Parse.Cloud.beforeSave` and `Parse.Cloud.afterSave` for Parse Config ([#9232](https://github.com/parse-community/parse-server/issues/9232)) ([90a1e4a](https://github.com/parse-community/parse-server/commit/90a1e4a200423d644efb3f0ba2fba4b99f5cf954))
|
||||
* Add Node 22 support ([#9187](https://github.com/parse-community/parse-server/issues/9187)) ([7778471](https://github.com/parse-community/parse-server/commit/7778471999c7e42236ce404229660d80ecc2acd6))
|
||||
* Add support for asynchronous invocation of `FilesAdapter.getFileLocation` ([#9271](https://github.com/parse-community/parse-server/issues/9271)) ([1a2da40](https://github.com/parse-community/parse-server/commit/1a2da4055abe831b3017172fb75e16d7a8093873))
|
||||
|
||||
# [7.2.0](https://github.com/parse-community/parse-server/compare/7.1.0...7.2.0) (2024-07-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Invalid push notification tokens are not cleaned up from database for FCM API v2 ([#9173](https://github.com/parse-community/parse-server/issues/9173)) ([284da09](https://github.com/parse-community/parse-server/commit/284da09f4546356b37511a589fb5f64a3efffe79))
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for dot notation on array fields of Parse Object ([#9115](https://github.com/parse-community/parse-server/issues/9115)) ([cf4c880](https://github.com/parse-community/parse-server/commit/cf4c8807b9da87a0a5f9c94e5bdfcf17cda80cf4))
|
||||
* Upgrade to @parse/push-adapter 6.4.0 ([#9182](https://github.com/parse-community/parse-server/issues/9182)) ([ef1634b](https://github.com/parse-community/parse-server/commit/ef1634bf1f360429108d29b08032fc7961ff96a1))
|
||||
* Upgrade to Parse JS SDK 5.3.0 ([#9180](https://github.com/parse-community/parse-server/issues/9180)) ([dca187f](https://github.com/parse-community/parse-server/commit/dca187f91b93cbb362b22a3fb9ee38451799ff13))
|
||||
|
||||
# [7.1.0](https://github.com/parse-community/parse-server/compare/7.0.0...7.1.0) (2024-06-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `Parse.Cloud.startJob` and `Parse.Push.send` not returning status ID when setting Parse Server option `directAccess: true` ([#8766](https://github.com/parse-community/parse-server/issues/8766)) ([5b0efb2](https://github.com/parse-community/parse-server/commit/5b0efb22efe94c47f243cf8b1e6407ed5c5a67d3))
|
||||
* `Required` option not handled correctly for special fields (File, GeoPoint, Polygon) on GraphQL API mutations ([#8915](https://github.com/parse-community/parse-server/issues/8915)) ([907ad42](https://github.com/parse-community/parse-server/commit/907ad4267c228d26cfcefe7848b30ce85ba7ff8f))
|
||||
* Facebook Limited Login not working due to incorrect domain in JWT validation ([#9122](https://github.com/parse-community/parse-server/issues/9122)) ([9d0bd2b](https://github.com/parse-community/parse-server/commit/9d0bd2badd6e5f7429d1af00b118225752e5d86a))
|
||||
* Live query throws error when constraint `notEqualTo` is set to `null` ([#8835](https://github.com/parse-community/parse-server/issues/8835)) ([11d3e48](https://github.com/parse-community/parse-server/commit/11d3e484df862224c15d20f6171514948981ea90))
|
||||
* Parse Server option `extendSessionOnUse` not working for session lengths < 24 hours ([#9113](https://github.com/parse-community/parse-server/issues/9113)) ([0a054e6](https://github.com/parse-community/parse-server/commit/0a054e6b541fd5ab470bf025665f5f7d2acedaa0))
|
||||
* Rate limiting can fail when using Parse Server option `rateLimit.redisUrl` with clusters ([#8632](https://github.com/parse-community/parse-server/issues/8632)) ([c277739](https://github.com/parse-community/parse-server/commit/c27773962399f8e27691e3b8087e7e1d59516efd))
|
||||
* SQL injection when using Parse Server with PostgreSQL; fixes security vulnerability [GHSA-c2hr-cqg6-8j6r](https://github.com/parse-community/parse-server/security/advisories/GHSA-c2hr-cqg6-8j6r) ([#9167](https://github.com/parse-community/parse-server/issues/9167)) ([2edf1e4](https://github.com/parse-community/parse-server/commit/2edf1e4c0363af01e97a7fbc97694f851b7d1ff3))
|
||||
|
||||
### Features
|
||||
|
||||
* Add `silent` log level for Cloud Code ([#8803](https://github.com/parse-community/parse-server/issues/8803)) ([5f81efb](https://github.com/parse-community/parse-server/commit/5f81efb42964c4c2fa8bcafee9446a0122e3ce21))
|
||||
* Add server security check status `security.enableCheck` to Features Router ([#8679](https://github.com/parse-community/parse-server/issues/8679)) ([b07ec15](https://github.com/parse-community/parse-server/commit/b07ec153825882e97cc48dc84072c7f549f3238b))
|
||||
* Prevent Parse Server start in case of unknown option in server configuration ([#8987](https://github.com/parse-community/parse-server/issues/8987)) ([8758e6a](https://github.com/parse-community/parse-server/commit/8758e6abb9dbb68757bddcbd332ad25100c24a0e))
|
||||
* Upgrade to @parse/push-adapter 6.0.0 ([#9066](https://github.com/parse-community/parse-server/issues/9066)) ([18bdbf8](https://github.com/parse-community/parse-server/commit/18bdbf89c53a57648891ef582614ba7c2941e587))
|
||||
* Upgrade to @parse/push-adapter 6.2.0 ([#9127](https://github.com/parse-community/parse-server/issues/9127)) ([ca20496](https://github.com/parse-community/parse-server/commit/ca20496f28e5ec1294a7a23c8559df82b79b2a04))
|
||||
* Upgrade to Parse JS SDK 5.2.0 ([#9128](https://github.com/parse-community/parse-server/issues/9128)) ([665b8d5](https://github.com/parse-community/parse-server/commit/665b8d52d6cf5275179a5e1fb132c934edb53ecc))
|
||||
|
||||
# [7.0.0](https://github.com/parse-community/parse-server/compare/6.4.0...7.0.0) (2024-03-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* CacheAdapter does not connect when using a CacheAdapter with a JSON config ([#8633](https://github.com/parse-community/parse-server/issues/8633)) ([720d24e](https://github.com/parse-community/parse-server/commit/720d24e18540da35d50957f17be878316ec30318))
|
||||
* Conditional email verification not working in some cases if `verifyUserEmails`, `preventLoginWithUnverifiedEmail` set to functions ([#8838](https://github.com/parse-community/parse-server/issues/8838)) ([8e7a6b1](https://github.com/parse-community/parse-server/commit/8e7a6b1480c0117e6c73e7adc5a6619115a04e85))
|
||||
* Context not passed to Cloud Code Trigger `beforeFind` when using `Parse.Query.include` ([#8765](https://github.com/parse-community/parse-server/issues/8765)) ([7d32d89](https://github.com/parse-community/parse-server/commit/7d32d8934f3ae7af7a7d8b9cc6a829c7d73973d3))
|
||||
* Deny request if master key is not set in Parse Server option `masterKeyIps` regardless of ACL and CLP ([#8957](https://github.com/parse-community/parse-server/issues/8957)) ([a7b5b38](https://github.com/parse-community/parse-server/commit/a7b5b38418cbed9be3f4a7665f25b97f592663e1))
|
||||
* Docker image not published to Docker Hub on new release ([#8905](https://github.com/parse-community/parse-server/issues/8905)) ([a2ac8d1](https://github.com/parse-community/parse-server/commit/a2ac8d133c71cd7b61e5ef59c4be915cfea85db6))
|
||||
* Docker version releases by removing arm/v6 and arm/v7 support ([#8976](https://github.com/parse-community/parse-server/issues/8976)) ([1f62dd0](https://github.com/parse-community/parse-server/commit/1f62dd0f4e107b22a387692558a042ee26ce8703))
|
||||
* GraphQL file upload fails in case of use of pointer or relation ([#8721](https://github.com/parse-community/parse-server/issues/8721)) ([1aba638](https://github.com/parse-community/parse-server/commit/1aba6382c873fb489d4a898d301e6da9fb6aa61b))
|
||||
* Improve PostgreSQL injection detection; fixes security vulnerability [GHSA-6927-3vr9-fxf2](https://github.com/parse-community/parse-server/security/advisories/GHSA-6927-3vr9-fxf2) which affects Parse Server deployments using a Postgres database ([#8961](https://github.com/parse-community/parse-server/issues/8961)) ([cbefe77](https://github.com/parse-community/parse-server/commit/cbefe770a7260b54748a058b8a7389937dc35833))
|
||||
* Incomplete user object in `verifyEmail` function if both username and email are changed ([#8889](https://github.com/parse-community/parse-server/issues/8889)) ([1eb95ae](https://github.com/parse-community/parse-server/commit/1eb95aeb41a96250e582d79a703f6adcb403c08b))
|
||||
* Parse Server option `emailVerifyTokenReuseIfValid: true` generates new token on every email verification request ([#8885](https://github.com/parse-community/parse-server/issues/8885)) ([0023ce4](https://github.com/parse-community/parse-server/commit/0023ce448a5e9423337d0e1a25648bde1156bc95))
|
||||
* Parse Server option `fileExtensions` default value rejects file extensions that are less than 3 or more than 4 characters long ([#8699](https://github.com/parse-community/parse-server/issues/8699)) ([2760381](https://github.com/parse-community/parse-server/commit/276038118377c2b22381bcd8d30337203822121b))
|
||||
* Parse Server option `fileUpload.fileExtensions` fails to determine file extension if filename contains multiple dots ([#8754](https://github.com/parse-community/parse-server/issues/8754)) ([3d6d50e](https://github.com/parse-community/parse-server/commit/3d6d50e0afff18b95fb906914e2cebd3839b517a))
|
||||
* Security bump @babel/traverse from 7.20.5 to 7.23.2 ([#8777](https://github.com/parse-community/parse-server/issues/8777)) ([2d6b3d1](https://github.com/parse-community/parse-server/commit/2d6b3d18499179e99be116f25c0850d3f449509c))
|
||||
* Security upgrade graphql from 16.6.0 to 16.8.1 ([#8758](https://github.com/parse-community/parse-server/issues/8758)) ([71dfd8a](https://github.com/parse-community/parse-server/commit/71dfd8a7ece8c0dd1a66d03bb9420cfd39f4f9b1))
|
||||
* Server crashes on invalid Cloud Function or Cloud Job name; fixes security vulnerability [GHSA-6hh7-46r2-vf29](https://github.com/parse-community/parse-server/security/advisories/GHSA-6hh7-46r2-vf29) ([#9024](https://github.com/parse-community/parse-server/issues/9024)) ([9f6e342](https://github.com/parse-community/parse-server/commit/9f6e3429d3b326cf4e2994733c618d08032fac6e))
|
||||
* Server crashes when receiving an array of `Parse.Pointer` in the request body ([#8784](https://github.com/parse-community/parse-server/issues/8784)) ([66e3603](https://github.com/parse-community/parse-server/commit/66e36039d8af654cfa0284666c0ddd94975dcb52))
|
||||
* Username is `undefined` in email verification link on email change ([#8887](https://github.com/parse-community/parse-server/issues/8887)) ([e315c13](https://github.com/parse-community/parse-server/commit/e315c137bf41bedfa8f0df537f2c3f6ab45b7e60))
|
||||
|
||||
### Features
|
||||
|
||||
* Add `$setOnInsert` operator to `Parse.Server.database.update` ([#8791](https://github.com/parse-community/parse-server/issues/8791)) ([f630a45](https://github.com/parse-community/parse-server/commit/f630a45aa5e87bc73a81fded061400c199b71a29))
|
||||
* Add `installationId` to arguments for `verifyUserEmails`, `preventLoginWithUnverifiedEmail` ([#8836](https://github.com/parse-community/parse-server/issues/8836)) ([a22dbe1](https://github.com/parse-community/parse-server/commit/a22dbe16d5ac0090608f6caaf0ebd134925b7fd4))
|
||||
* Add `installationId`, `ip`, `resendRequest` to arguments passed to `verifyUserEmails` on verification email request ([#8873](https://github.com/parse-community/parse-server/issues/8873)) ([8adcbee](https://github.com/parse-community/parse-server/commit/8adcbee11283d3e95179ca2047e2615f52c18806))
|
||||
* Add `Parse.User` as function parameter to Parse Server options `verifyUserEmails`, `preventLoginWithUnverifiedEmail` on login ([#8850](https://github.com/parse-community/parse-server/issues/8850)) ([972f630](https://github.com/parse-community/parse-server/commit/972f6300163b3cd7d95eeb95986e8322c95f821c))
|
||||
* Add compatibility for MongoDB Atlas Serverless and AWS Amazon DocumentDB with collation options `enableCollationCaseComparison`, `transformEmailToLowercase`, `transformUsernameToLowercase` ([#8805](https://github.com/parse-community/parse-server/issues/8805)) ([09fbeeb](https://github.com/parse-community/parse-server/commit/09fbeebba8870e7cf371fb84371a254c7b368620))
|
||||
* Add context to Cloud Code Triggers `beforeLogin` and `afterLogin` ([#8724](https://github.com/parse-community/parse-server/issues/8724)) ([a9c34ef](https://github.com/parse-community/parse-server/commit/a9c34ef1e2c78a42fb8b5fa8d569b7677c74919d))
|
||||
* Add password validation via POST request for user with unverified email using master key and option `ignoreEmailVerification` ([#8895](https://github.com/parse-community/parse-server/issues/8895)) ([633a9d2](https://github.com/parse-community/parse-server/commit/633a9d25e4253e2125bc93c02ee8a37e0f5f7b83))
|
||||
* Add support for MongoDB 7 ([#8761](https://github.com/parse-community/parse-server/issues/8761)) ([3de8494](https://github.com/parse-community/parse-server/commit/3de8494a221991dfd10a74e0a2dc89576265c9b7))
|
||||
* Add support for MongoDB query comment ([#8928](https://github.com/parse-community/parse-server/issues/8928)) ([2170962](https://github.com/parse-community/parse-server/commit/2170962a50fa353ed85eda3f11dce7ee3647b087))
|
||||
* Add support for Node 20, drop support for Node 14, 16 ([#8907](https://github.com/parse-community/parse-server/issues/8907)) ([ced4872](https://github.com/parse-community/parse-server/commit/ced487246ea0ef72a8aa014991f003209b34841e))
|
||||
* Add support for Postgres 16 ([#8898](https://github.com/parse-community/parse-server/issues/8898)) ([99489b2](https://github.com/parse-community/parse-server/commit/99489b22e4f0982e6cb39992974b51aa8d3a31e4))
|
||||
* Allow `Parse.Session.current` on expired session token instead of throwing error ([#8722](https://github.com/parse-community/parse-server/issues/8722)) ([f9dde4a](https://github.com/parse-community/parse-server/commit/f9dde4a9f8a90c63f71172c9bc515b0f6c6d2e4a))
|
||||
* Allow setting `createdAt` and `updatedAt` during `Parse.Object` creation with maintenance key ([#8696](https://github.com/parse-community/parse-server/issues/8696)) ([77bbfb3](https://github.com/parse-community/parse-server/commit/77bbfb3f186f5651c33ba152f04cff95128eaf2d))
|
||||
* Deprecation DEPPS5: Config option `allowClientClassCreation` defaults to `false` ([#8849](https://github.com/parse-community/parse-server/issues/8849)) ([29624e0](https://github.com/parse-community/parse-server/commit/29624e0fae17161cd412ae58d35a195cfa286cad))
|
||||
* Deprecation DEPPS6: Authentication adapters disabled by default ([#8858](https://github.com/parse-community/parse-server/issues/8858)) ([0cf58eb](https://github.com/parse-community/parse-server/commit/0cf58eb8d60c8e5f485764e154f3214c49eee430))
|
||||
* Deprecation DEPPS7: Remove deprecated Cloud Code file trigger syntax ([#8855](https://github.com/parse-community/parse-server/issues/8855)) ([4e6a375](https://github.com/parse-community/parse-server/commit/4e6a375b5184ae0f7aa256a921eca4021c609435))
|
||||
* Deprecation DEPPS8: Parse Server option `allowExpiredAuthDataToken` defaults to `false` ([#8860](https://github.com/parse-community/parse-server/issues/8860)) ([e29845f](https://github.com/parse-community/parse-server/commit/e29845f8dacac09ce3093d75c0d92330c24389e8))
|
||||
* Deprecation DEPPS9: LiveQuery `fields` option is renamed to `keys` ([#8852](https://github.com/parse-community/parse-server/issues/8852)) ([38983e8](https://github.com/parse-community/parse-server/commit/38983e8e9b5cdbd006f311a2338103624137d013))
|
||||
* Node process exits with error code 1 on uncaught exception to allow custom uncaught exception handling ([#8894](https://github.com/parse-community/parse-server/issues/8894)) ([70c280c](https://github.com/parse-community/parse-server/commit/70c280ca578ff28b5acf92f37fbe06d42a5b34ca))
|
||||
* Switch GraphQL server from Yoga v2 to Apollo v4 ([#8959](https://github.com/parse-community/parse-server/issues/8959)) ([105ae7c](https://github.com/parse-community/parse-server/commit/105ae7c8a57d5a650b243174a80c26bf6db16e28))
|
||||
* Upgrade Parse Server Push Adapter to 5.0.2 ([#8813](https://github.com/parse-community/parse-server/issues/8813)) ([6ef1986](https://github.com/parse-community/parse-server/commit/6ef1986c03a1d84b7e11c05851e5bf9688d88740))
|
||||
* Upgrade to Parse JS SDK 5 ([#9022](https://github.com/parse-community/parse-server/issues/9022)) ([ad4aa83](https://github.com/parse-community/parse-server/commit/ad4aa83983205a0e27639f6ee6a4a5963b67e4b8))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* Improved IP validation performance for `masterKeyIPs`, `maintenanceKeyIPs` ([#8510](https://github.com/parse-community/parse-server/issues/8510)) ([b87daba](https://github.com/parse-community/parse-server/commit/b87daba0671a1b0b7b8d63bc671d665c91a04522))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* The Parse Server option `allowClientClassCreation` defaults to `false`. ([29624e0](29624e0))
|
||||
* A request using the master key will now be rejected as unauthorized if the IP from which the request originates is not set in the Parse Server option `masterKeyIps`, even if the request does not require the master key permission, for example for a public object in a public class class. ([a7b5b38](a7b5b38))
|
||||
* Node process now exits with code 1 on uncaught exceptions, enabling custom handlers that were blocked by Parse Server's default behavior of re-throwing errors. This change may lead to automatic process restarts by the environment, unlike before. ([70c280c](70c280c))
|
||||
* Authentication adapters are disabled by default; to use an authentication adapter it needs to be explicitly enabled in the Parse Server authentication adapter option `auth.<provider>.enabled: true` ([0cf58eb](0cf58eb))
|
||||
* Parse Server option `allowExpiredAuthDataToken` defaults to `false`; a 3rd party authentication token will be validated every time the user tries to log in and the login will fail if the token has expired; the effect of this change may differ for different authentication adapters, depending on the token lifetime and the token refresh logic of the adapter ([e29845f](e29845f))
|
||||
* LiveQuery `fields` option is renamed to `keys` ([38983e8](38983e8))
|
||||
* Cloud Code file trigger syntax has been aligned with object trigger syntax, for example `Parse.Cloud.beforeDeleteFile'` has been changed to `Parse.Cloud.beforeDelete(Parse.File, (request) => {})'` ([4e6a375](4e6a375))
|
||||
* Removes support for Node 14 and 16 ([ced4872](ced4872))
|
||||
* Removes support for Postgres 11 and 12 ([99489b2](99489b2))
|
||||
* The `Parse.User` passed as argument if `verifyUserEmails` is set to a function is renamed from `user` to `object` for consistency with invocations of `verifyUserEmails` on signup or login; the user object is not a plain JavaScript object anymore but an instance of `Parse.User` ([8adcbee](8adcbee))
|
||||
* `Parse.Session.current()` no longer throws an error if the session token is expired, but instead returns the session token with its expiration date to allow checking its validity ([f9dde4a](f9dde4a))
|
||||
* `Parse.Query` no longer supports the BSON type `code`; although this feature was never officially documented, its removal is announced as a breaking change to protect deployments where it might be in use. ([3de8494](3de8494))
|
||||
* Improve PostgreSQL injection detection; fixes security vulnerability [GHSA-6927-3vr9-fxf2](https://github.com/parse-community/parse-server/security/advisories/GHSA-6927-3vr9-fxf2) which affects Parse Server deployments using a Postgres database ([#8960](https://github.com/parse-community/parse-server/issues/8960)) ([a6e6549](https://github.com/parse-community/parse-server/commit/a6e654943536932904a69b51e513507fcf90a504))
|
||||
|
||||
# [6.4.0](https://github.com/parse-community/parse-server/compare/6.3.1...6.4.0) (2023-11-16)
|
||||
|
||||
|
||||
@@ -220,7 +220,6 @@ class CiVersionCheck {
|
||||
* Runs the check.
|
||||
*/
|
||||
async check() {
|
||||
/* eslint-disable no-console */
|
||||
try {
|
||||
console.log(`\nChecking ${this.packageName} versions in CI environments...`);
|
||||
|
||||
@@ -285,7 +284,6 @@ class CiVersionCheck {
|
||||
const msg = `Failed to check ${this.packageName} versions with error: ${e}`;
|
||||
core.setFailed(msg);
|
||||
}
|
||||
/* eslint-enable no-console */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+18
-20
@@ -1,7 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
const CiVersionCheck = require('./CiVersionCheck');
|
||||
const { exec } = require('child_process');
|
||||
const mongoVersionList = require('mongodb-version-list');
|
||||
const allNodeVersions = require('all-node-versions');
|
||||
|
||||
async function check() {
|
||||
// Run checks
|
||||
@@ -13,16 +14,14 @@ async function check() {
|
||||
* Check the MongoDB versions used in test environments.
|
||||
*/
|
||||
async function checkMongoDbVersions() {
|
||||
let latestStableVersions = await new Promise((resolve, reject) => {
|
||||
exec('m ls', (error, stdout) => {
|
||||
const releasedVersions = await new Promise((resolve, reject) => {
|
||||
mongoVersionList(function (error, versions) {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve(stdout.trim());
|
||||
resolve(versions);
|
||||
});
|
||||
});
|
||||
latestStableVersions = latestStableVersions.split('\n').map(version => version.trim());
|
||||
|
||||
await new CiVersionCheck({
|
||||
packageName: 'MongoDB',
|
||||
@@ -30,14 +29,13 @@ async function checkMongoDbVersions() {
|
||||
yamlFilePath: './.github/workflows/ci.yml',
|
||||
ciEnvironmentsKeyPath: 'jobs.check-mongo.strategy.matrix.include',
|
||||
ciVersionKey: 'MONGODB_VERSION',
|
||||
releasedVersions: latestStableVersions,
|
||||
latestComponent: CiVersionCheck.versionComponents.patch,
|
||||
releasedVersions,
|
||||
latestComponent: CiVersionCheck.versionComponents.minor,
|
||||
ignoreReleasedVersions: [
|
||||
'<4.2.0', // These versions have reached their end-of-life support date
|
||||
'>=4.3.0 <5.0.0', // Unsupported rapid release versions
|
||||
'>=5.1.0 <6.0.0', // Unsupported rapid release versions
|
||||
'>=6.1.0 <7.0.0', // Unsupported rapid release versions
|
||||
'>=7.1.0 <8.0.0', // Unsupported rapid release versions
|
||||
'<4.0.0', // Versions reached their MongoDB end-of-life support date
|
||||
'~4.1.0', // Development release according to MongoDB support
|
||||
'~4.3.0', // Development release according to MongoDB support
|
||||
'~4.7.0', // Development release according to MongoDB support
|
||||
],
|
||||
}).check();
|
||||
}
|
||||
@@ -46,9 +44,8 @@ async function checkMongoDbVersions() {
|
||||
* Check the Nodejs versions used in test environments.
|
||||
*/
|
||||
async function checkNodeVersions() {
|
||||
const allVersions = (await import('all-node-versions')).default;
|
||||
const { versions } = await allVersions();
|
||||
const nodeVersions = versions.map(version => version.node);
|
||||
const allVersions = await allNodeVersions();
|
||||
const releasedVersions = allVersions.versions;
|
||||
|
||||
await new CiVersionCheck({
|
||||
packageName: 'Node.js',
|
||||
@@ -56,12 +53,13 @@ async function checkNodeVersions() {
|
||||
yamlFilePath: './.github/workflows/ci.yml',
|
||||
ciEnvironmentsKeyPath: 'jobs.check-mongo.strategy.matrix.include',
|
||||
ciVersionKey: 'NODE_VERSION',
|
||||
releasedVersions: nodeVersions,
|
||||
releasedVersions,
|
||||
latestComponent: CiVersionCheck.versionComponents.minor,
|
||||
ignoreReleasedVersions: [
|
||||
'<18.0.0', // These versions have reached their end-of-life support date
|
||||
'>=19.0.0 <20.0.0', // These versions have reached their end-of-life support date
|
||||
'>=21.0.0', // These versions are not officially supported yet
|
||||
'<12.0.0', // These versions have reached their end-of-life support date
|
||||
'>=13.0.0 <14.0.0', // These versions have reached their end-of-life support date
|
||||
'>=15.0.0 <16.0.0', // These versions have reached their end-of-life support date
|
||||
'>=19.0.0', // These versions are not officially supported yet
|
||||
],
|
||||
}).check();
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ const util = require('util');
|
||||
fs.readFile('./src/Options/docs.js', 'utf8'),
|
||||
]);
|
||||
if (currentDefinitions !== newDefinitions || currentDocs !== newDocs) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
'\x1b[31m%s\x1b[0m',
|
||||
'Definitions files cannot be updated manually. Please update src/Options/index.js then run `npm run definitions` to generate definitions.'
|
||||
|
||||
@@ -86,8 +86,7 @@ class NodeEngineCheck {
|
||||
file: file,
|
||||
nodeVersion: version
|
||||
});
|
||||
} catch {
|
||||
// eslint-disable-next-line no-console
|
||||
} catch(e) {
|
||||
console.log(`Ignoring file because it is not valid JSON: ${file}`);
|
||||
core.warning(`Ignoring file because it is not valid JSON: ${file}`);
|
||||
}
|
||||
@@ -172,7 +171,6 @@ async function check() {
|
||||
// Get highest version
|
||||
const highestVersion = higherVersions.map(v => v.nodeMinVersion).pop();
|
||||
|
||||
/* eslint-disable no-console */
|
||||
// If there are higher versions
|
||||
if (higherVersions.length > 0) {
|
||||
console.log(`\nThere are ${higherVersions.length} dependencies that require a higher node engine version than the parent package (${parentVersion.nodeVersion}):`);
|
||||
@@ -191,7 +189,6 @@ async function check() {
|
||||
}
|
||||
|
||||
console.log(`✅ All dependencies satisfy the node version requirement of the parent package (${parentVersion.nodeVersion}).`);
|
||||
/* eslint-enable no-console */
|
||||
}
|
||||
|
||||
check();
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Read package exclusion list from arguments
|
||||
exclusionList=("$@")
|
||||
|
||||
# Convert exclusion list to grep pattern
|
||||
exclusionPattern=$(printf "|%s" "${exclusionList[@]}")
|
||||
exclusionPattern=${exclusionPattern:1}
|
||||
|
||||
# Get list of all dev dependencies
|
||||
devDeps=$(jq -r '.devDependencies | keys | .[]' package.json)
|
||||
|
||||
# Filter out exclusion list
|
||||
depsToUninstall=$(echo "$devDeps" | grep -Ev "$exclusionPattern")
|
||||
|
||||
# If there are dependencies to uninstall then uninstall them
|
||||
if [ -n "$depsToUninstall" ]; then
|
||||
echo "Uninstalling dev dependencies: $depsToUninstall"
|
||||
npm uninstall $depsToUninstall
|
||||
else
|
||||
echo "No dev dependencies to uninstall"
|
||||
fi
|
||||
@@ -1,47 +0,0 @@
|
||||
const js = require("@eslint/js");
|
||||
const babelParser = require("@babel/eslint-parser");
|
||||
const globals = require("globals");
|
||||
const unusedImports = require("eslint-plugin-unused-imports");
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
ignores: ["**/lib/**", "**/coverage/**", "**/out/**", "**/types/**"],
|
||||
},
|
||||
js.configs.recommended,
|
||||
{
|
||||
languageOptions: {
|
||||
parser: babelParser,
|
||||
ecmaVersion: 6,
|
||||
sourceType: "module",
|
||||
globals: {
|
||||
Parse: "readonly",
|
||||
...globals.node,
|
||||
},
|
||||
parserOptions: {
|
||||
requireConfigFile: false,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
"unused-imports": unusedImports,
|
||||
},
|
||||
rules: {
|
||||
indent: ["error", 2, { SwitchCase: 1 }],
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"unused-imports/no-unused-vars": "error",
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"no-trailing-spaces": "error",
|
||||
"eol-last": "error",
|
||||
"space-in-parens": ["error", "never"],
|
||||
"no-multiple-empty-lines": "warn",
|
||||
"prefer-const": "error",
|
||||
"space-infix-ops": "error",
|
||||
"no-useless-escape": "off",
|
||||
"require-atomic-updates": "off",
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
curly: ["error", "all"],
|
||||
"block-spacing": ["error", "always"],
|
||||
"no-unused-vars": "off",
|
||||
"no-console": "warn"
|
||||
},
|
||||
},
|
||||
];
|
||||
+6
-6
@@ -6,10 +6,10 @@
|
||||
"source": {
|
||||
"include": [
|
||||
"README.md",
|
||||
"./lib/cloud-code",
|
||||
"./lib/Options/docs.js",
|
||||
"./lib/ParseServer.js",
|
||||
"./lib/Adapters"
|
||||
"./src/cloud-code",
|
||||
"./src/Options/docs.js",
|
||||
"./src/ParseServer.js",
|
||||
"./src/Adapters"
|
||||
],
|
||||
"excludePattern": "(^|\\/|\\\\)_"
|
||||
},
|
||||
@@ -29,8 +29,8 @@
|
||||
"template": "./node_modules/clean-jsdoc-theme",
|
||||
"theme_opts": {
|
||||
"default_theme": "dark",
|
||||
"title": "<img src='https://raw.githubusercontent.com/parse-community/parse-server/alpha/.github/parse-server-logo.png' class='logo'/>",
|
||||
"create_style": "header, .sidebar-section-title, .sidebar-title { color: #139cee !important } .logo { margin-left : 40px; margin-right: 40px; height: auto; max-width: 100%; object-fit: contain; }"
|
||||
"title": "<img src='../.github/parse-server-logo.png' class='logo'/>",
|
||||
"create_style": "header, .sidebar-section-title, .sidebar-title { color: #139cee !important } .logo { margin-left : 40px; margin-right: 40px }"
|
||||
}
|
||||
},
|
||||
"markdown": {
|
||||
|
||||
Generated
+19689
-20775
File diff suppressed because it is too large
Load Diff
+90
-92
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "parse-server",
|
||||
"version": "8.6.53",
|
||||
"version": "6.5.11",
|
||||
"description": "An express module providing a Parse-compatible API server",
|
||||
"main": "lib/index.js",
|
||||
"repository": {
|
||||
@@ -15,98 +15,97 @@
|
||||
"LICENSE",
|
||||
"NOTICE",
|
||||
"postinstall.js",
|
||||
"README.md",
|
||||
"types"
|
||||
"README.md"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@apollo/server": "4.12.1",
|
||||
"@graphql-tools/merge": "9.0.24",
|
||||
"@graphql-tools/schema": "10.0.23",
|
||||
"@graphql-tools/utils": "10.8.6",
|
||||
"@parse/fs-files-adapter": "3.0.0",
|
||||
"@parse/push-adapter": "6.11.0",
|
||||
"bcryptjs": "3.0.2",
|
||||
"commander": "13.1.0",
|
||||
"@babel/eslint-parser": "7.24.1",
|
||||
"@graphql-tools/merge": "8.4.1",
|
||||
"@graphql-tools/schema": "9.0.4",
|
||||
"@graphql-tools/utils": "8.12.0",
|
||||
"@graphql-yoga/node": "2.6.0",
|
||||
"@parse/fs-files-adapter": "2.0.1",
|
||||
"@parse/push-adapter": "5.1.1",
|
||||
"bcryptjs": "2.4.3",
|
||||
"body-parser": "1.20.3",
|
||||
"commander": "10.0.1",
|
||||
"cors": "2.8.5",
|
||||
"express": "5.2.1",
|
||||
"express-rate-limit": "7.5.1",
|
||||
"follow-redirects": "1.15.9",
|
||||
"graphql": "16.11.0",
|
||||
"deepcopy": "2.1.0",
|
||||
"express": "4.21.1",
|
||||
"express-rate-limit": "6.7.0",
|
||||
"follow-redirects": "1.15.6",
|
||||
"graphql": "16.8.1",
|
||||
"graphql-list-fields": "2.0.4",
|
||||
"graphql-relay": "0.10.2",
|
||||
"graphql-upload": "15.0.2",
|
||||
"graphql-relay": "0.10.1",
|
||||
"graphql-tag": "2.12.6",
|
||||
"intersect": "1.0.1",
|
||||
"jsonwebtoken": "9.0.2",
|
||||
"jwks-rsa": "3.2.0",
|
||||
"ldapjs": "3.0.7",
|
||||
"jsonwebtoken": "9.0.0",
|
||||
"jwks-rsa": "3.1.0",
|
||||
"ldapjs": "2.3.3",
|
||||
"lodash": "4.17.21",
|
||||
"lru-cache": "10.4.0",
|
||||
"mime": "4.0.7",
|
||||
"mongodb": "6.20.0",
|
||||
"lru-cache": "10.1.0",
|
||||
"mime": "3.0.0",
|
||||
"mongodb": "4.10.0",
|
||||
"mustache": "4.2.0",
|
||||
"otpauth": "9.4.0",
|
||||
"parse": "7.1.2",
|
||||
"path-to-regexp": "6.3.0",
|
||||
"pg-monitor": "3.0.0",
|
||||
"pg-promise": "12.2.0",
|
||||
"otpauth": "9.2.2",
|
||||
"parse": "4.2.0",
|
||||
"path-to-regexp": "6.2.2",
|
||||
"pg-monitor": "2.0.0",
|
||||
"pg-promise": "11.5.4",
|
||||
"pluralize": "8.0.0",
|
||||
"punycode": "2.3.1",
|
||||
"rate-limit-redis": "4.2.0",
|
||||
"redis": "4.7.0",
|
||||
"semver": "7.7.2",
|
||||
"rate-limit-redis": "3.0.2",
|
||||
"redis": "4.6.13",
|
||||
"semver": "7.5.4",
|
||||
"subscriptions-transport-ws": "0.11.0",
|
||||
"tv4": "1.3.0",
|
||||
"uuid": "11.1.0",
|
||||
"winston": "3.17.0",
|
||||
"uuid": "9.0.1",
|
||||
"winston": "3.12.0",
|
||||
"winston-daily-rotate-file": "5.0.0",
|
||||
"ws": "8.18.2"
|
||||
"ws": "8.17.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/core": "1.11.1",
|
||||
"@apollo/client": "3.13.8",
|
||||
"@babel/cli": "7.27.0",
|
||||
"@babel/core": "7.27.4",
|
||||
"@babel/eslint-parser": "7.28.0",
|
||||
"@babel/plugin-proposal-object-rest-spread": "7.20.7",
|
||||
"@babel/plugin-transform-flow-strip-types": "7.26.5",
|
||||
"@babel/preset-env": "7.27.2",
|
||||
"@babel/preset-typescript": "7.27.1",
|
||||
"@saithodev/semantic-release-backmerge": "4.0.1",
|
||||
"@semantic-release/changelog": "6.0.3",
|
||||
"@semantic-release/commit-analyzer": "13.0.1",
|
||||
"@semantic-release/git": "10.0.1",
|
||||
"@semantic-release/github": "11.0.2",
|
||||
"@semantic-release/npm": "12.0.1",
|
||||
"@semantic-release/release-notes-generator": "14.0.3",
|
||||
"all-node-versions": "13.0.1",
|
||||
"apollo-upload-client": "18.0.1",
|
||||
"clean-jsdoc-theme": "4.3.0",
|
||||
"cross-env": "7.0.3",
|
||||
"@actions/core": "1.9.1",
|
||||
"@apollo/client": "3.6.1",
|
||||
"@babel/cli": "7.10.0",
|
||||
"@babel/core": "7.20.2",
|
||||
"@babel/plugin-proposal-object-rest-spread": "7.10.0",
|
||||
"@babel/plugin-transform-flow-strip-types": "7.9.0",
|
||||
"@babel/preset-env": "7.10.0",
|
||||
"@saithodev/semantic-release-backmerge": "2.1.2",
|
||||
"@semantic-release/changelog": "5.0.1",
|
||||
"@semantic-release/commit-analyzer": "8.0.1",
|
||||
"@semantic-release/git": "9.0.0",
|
||||
"@semantic-release/github": "7.2.3",
|
||||
"@semantic-release/npm": "7.1.3",
|
||||
"@semantic-release/release-notes-generator": "9.0.3",
|
||||
"all-node-versions": "11.3.0",
|
||||
"apollo-upload-client": "17.0.0",
|
||||
"bcrypt-nodejs": "0.0.3",
|
||||
"clean-jsdoc-theme": "4.2.7",
|
||||
"cross-env": "7.0.2",
|
||||
"deep-diff": "1.0.2",
|
||||
"eslint": "9.27.0",
|
||||
"eslint-plugin-expect-type": "0.6.2",
|
||||
"eslint-plugin-unused-imports": "4.3.0",
|
||||
"form-data": "4.0.4",
|
||||
"globals": "16.2.0",
|
||||
"eslint": "8.26.0",
|
||||
"eslint-plugin-flowtype": "8.0.3",
|
||||
"flow-bin": "0.119.1",
|
||||
"form-data": "3.0.0",
|
||||
"graphql-tag": "2.12.6",
|
||||
"jasmine": "5.7.1",
|
||||
"husky": "4.3.8",
|
||||
"jasmine": "3.5.0",
|
||||
"jasmine-spec-reporter": "7.0.0",
|
||||
"jsdoc": "4.0.4",
|
||||
"jsdoc": "3.6.3",
|
||||
"jsdoc-babel": "0.5.0",
|
||||
"lint-staged": "16.1.0",
|
||||
"m": "1.9.1",
|
||||
"madge": "8.0.0",
|
||||
"lint-staged": "10.2.3",
|
||||
"madge": "5.0.1",
|
||||
"mock-files-adapter": "file:spec/dependencies/mock-files-adapter",
|
||||
"mock-mail-adapter": "file:spec/dependencies/mock-mail-adapter",
|
||||
"mongodb-runner": "5.9.3",
|
||||
"node-abort-controller": "3.1.1",
|
||||
"mongodb-runner": "4.8.1",
|
||||
"mongodb-version-list": "1.0.0",
|
||||
"node-abort-controller": "3.0.1",
|
||||
"node-fetch": "3.2.10",
|
||||
"nyc": "17.1.0",
|
||||
"nyc": "15.1.0",
|
||||
"prettier": "2.0.5",
|
||||
"semantic-release": "24.2.5",
|
||||
"typescript": "5.8.3",
|
||||
"typescript-eslint": "8.33.1",
|
||||
"yaml": "2.8.0"
|
||||
"semantic-release": "17.4.6",
|
||||
"yaml": "1.10.0"
|
||||
},
|
||||
"scripts": {
|
||||
"ci:check": "node ./ci/ciCheck.js",
|
||||
@@ -114,39 +113,38 @@
|
||||
"ci:definitionsCheck": "node ./ci/definitionsCheck.js",
|
||||
"definitions": "node ./resources/buildConfigDefinitions.js && prettier --write 'src/Options/*.js'",
|
||||
"docs": "jsdoc -c ./jsdoc-conf.json",
|
||||
"lint": "eslint --cache ./ --flag unstable_config_lookup_from_file",
|
||||
"lint-fix": "eslint --fix --cache ./ --flag unstable_config_lookup_from_file",
|
||||
"build": "babel src/ -d lib/ --copy-files --extensions '.ts,.js'",
|
||||
"build:types": "tsc",
|
||||
"lint": "flow && eslint --cache ./",
|
||||
"lint-fix": "eslint --fix --cache ./",
|
||||
"build": "babel src/ -d lib/ --copy-files",
|
||||
"watch": "babel --watch src/ -d lib/ --copy-files",
|
||||
"watch:ts": "tsc --watch",
|
||||
"test:mongodb:6.0.19": "MONGODB_VERSION=6.0.19 npm run test",
|
||||
"test:mongodb:7.0.16": "MONGODB_VERSION=7.0.16 npm run test",
|
||||
"test:mongodb:8.0.4": "MONGODB_VERSION=8.0.4 npm run test",
|
||||
"test:mongodb:runnerstart": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=$npm_config_dbversion} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner start",
|
||||
"test:mongodb:testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=$npm_config_dbversion} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 jasmine",
|
||||
"test:mongodb": "npm run test:mongodb:runnerstart --dbversion=$npm_config_dbversion && npm run test:mongodb:testonly --dbversion=$npm_config_dbversion",
|
||||
"test:mongodb:4.2.19": "npm run test:mongodb --dbversion=4.2.19",
|
||||
"test:mongodb:4.4.13": "npm run test:mongodb --dbversion=4.4.13",
|
||||
"test:mongodb:5.3.2": "npm run test:mongodb --dbversion=5.3.2",
|
||||
"test:mongodb:6.0.2": "npm run test:mongodb --dbversion=6.0.2",
|
||||
"test:postgres:testonly": "cross-env PARSE_SERVER_TEST_DB=postgres PARSE_SERVER_TEST_DATABASE_URI=postgres://postgres:password@localhost:5432/parse_server_postgres_adapter_test_database npm run testonly",
|
||||
"testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=8.0.4} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 jasmine",
|
||||
"test": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=8.0.4} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner exec -t ${MONGODB_TOPOLOGY} --version ${MONGODB_VERSION} -- --port 27017 -- npm run testonly",
|
||||
"test:types": "eslint types/tests.ts -c ./types/eslint.config.mjs",
|
||||
"coverage:mongodb": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=8.0.4} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner exec -t ${MONGODB_TOPOLOGY} --version ${MONGODB_VERSION} -- --port 27017 -- npm run coverage",
|
||||
"coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=8.0.4} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 nyc jasmine",
|
||||
"posttest:mongodb": "mongodb-runner stop",
|
||||
"pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=6.0.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner start",
|
||||
"testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 jasmine",
|
||||
"test": "npm run testonly",
|
||||
"posttest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner stop",
|
||||
"coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 nyc jasmine",
|
||||
"start": "node ./bin/parse-server",
|
||||
"prettier": "prettier --write {src,spec}/{**/*,*}.js",
|
||||
"prepare": "npm run build",
|
||||
"postinstall": "node -p 'require(\"./postinstall.js\")()'",
|
||||
"madge:circular": "node_modules/.bin/madge ./src --circular",
|
||||
"benchmark": "cross-env MONGODB_VERSION=8.0.4 MONGODB_TOPOLOGY=standalone mongodb-runner exec -t standalone --version 8.0.4 -- --port 27017 -- npm run benchmark:only",
|
||||
"benchmark:only": "node benchmark/performance.js",
|
||||
"benchmark:quick": "cross-env BENCHMARK_ITERATIONS=10 npm run benchmark:only"
|
||||
"madge:circular": "node_modules/.bin/madge ./src --circular"
|
||||
},
|
||||
"types": "types/index.d.ts",
|
||||
"engines": {
|
||||
"node": ">=18.20.4 <19.0.0 || >=20.18.0 <21.0.0 || >=22.12.0 <23.0.0 || >=24.11.0 <25.0.0"
|
||||
"node": ">=14.21.0 <17 || >=18 <19"
|
||||
},
|
||||
"bin": {
|
||||
"parse-server": "bin/parse-server"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@node-rs/bcrypt": "1.10.7"
|
||||
"@node-rs/bcrypt": "1.1.0"
|
||||
},
|
||||
"collective": {
|
||||
"type": "opencollective",
|
||||
|
||||
+46
-34
@@ -1,38 +1,50 @@
|
||||
const message = `
|
||||
1111111111
|
||||
1111111111111111
|
||||
1111111111111111111111
|
||||
11111111111111111111111111
|
||||
111111111111111 11111111
|
||||
1111111111111 111 111111
|
||||
1111111111111 111111111 111111
|
||||
111111111111 11111111111 111111
|
||||
1111111111111 11111111111 111111
|
||||
1111111111111 1111111111 111111
|
||||
1111111111111111111111111 1111111
|
||||
11111111 11111111
|
||||
111111 111 1111111111111111111
|
||||
11111 11111 111111111111111111
|
||||
11111 1 11111111111111111
|
||||
111111 111111111111111111
|
||||
11111111111111111111111111
|
||||
1111111111111111111111
|
||||
111111111111111111
|
||||
11111111111
|
||||
|
||||
Thank you for using Parse Platform!
|
||||
https://parseplatform.org
|
||||
|
||||
Please consider donating to help us maintain
|
||||
this package:
|
||||
const pkg = require('./package.json');
|
||||
|
||||
👉 https://opencollective.com/parse-server 👈
|
||||
const version = parseFloat(process.version.substring(1));
|
||||
const minimum = parseFloat(pkg.engines.node.match(/\d+/g).join('.'));
|
||||
|
||||
`;
|
||||
module.exports = function () {
|
||||
const openCollective = `
|
||||
1111111111
|
||||
1111111111111111
|
||||
1111111111111111111111
|
||||
11111111111111111111111111
|
||||
111111111111111 11111111
|
||||
1111111111111 111111
|
||||
1111111111111 111111111 111111
|
||||
111111111111 11111111111 111111
|
||||
1111111111111 11111111111 111111
|
||||
1111111111111 1111111111 111111
|
||||
1111111111111111111111111 1111111
|
||||
11111111 11111111
|
||||
111111 1111111111111111111
|
||||
11111 11111 111111111111111111
|
||||
11111 11111111111111111
|
||||
111111 111111111111111111
|
||||
11111111111111111111111111
|
||||
1111111111111111111111
|
||||
111111111111111111
|
||||
11111111111
|
||||
|
||||
function main() {
|
||||
process.stdout.write(message);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
module.exports = main;
|
||||
Thanks for installing parse 🙏
|
||||
Please consider donating to our open collective
|
||||
to help us maintain this package.
|
||||
|
||||
👉 https://opencollective.com/parse-server
|
||||
|
||||
`;
|
||||
process.stdout.write(openCollective);
|
||||
if (version >= minimum) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const errorMessage = `
|
||||
⚠️ parse-server requires at least node@${minimum}!
|
||||
You have node@${version}
|
||||
|
||||
`;
|
||||
|
||||
process.stdout.write(errorMessage);
|
||||
process.exit(1);
|
||||
};
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
<body>
|
||||
<h1>{{appName}}</h1>
|
||||
<h1>Expired verification link!</h1>
|
||||
<form method="POST" action="{{publicServerUrl}}/apps/{{appId}}/resend_verification_email">
|
||||
<input name="token" type="hidden" value="{{token}}">
|
||||
<input name="locale" type="hidden" value="{{locale}}">
|
||||
<form method="POST" action="{{{publicServerUrl}}}/apps/{{{appId}}}/resend_verification_email">
|
||||
<input name="username" type="hidden" value="{{{username}}}">
|
||||
<input name="locale" type="hidden" value="{{{locale}}}">
|
||||
<button type="submit">Resend Link</button>
|
||||
</form>
|
||||
</body>
|
||||
|
||||
@@ -23,11 +23,11 @@
|
||||
<p>You can set a new Password for your account: {{username}}</p>
|
||||
<br />
|
||||
<p>{{error}}</p>
|
||||
<form id='form' action='{{publicServerUrl}}/apps/{{appId}}/request_password_reset' method='POST'>
|
||||
<form id='form' action='{{{publicServerUrl}}}/apps/{{{appId}}}/request_password_reset' method='POST'>
|
||||
<input name='utf-8' type='hidden' value='✓' />
|
||||
<input name="username" type="hidden" id="username" value="{{username}}" />
|
||||
<input name="token" type="hidden" id="token" value="{{token}}" />
|
||||
<input name="locale" type="hidden" id="locale" value="{{locale}}" />
|
||||
<input name="username" type="hidden" id="username" value="{{{username}}}" />
|
||||
<input name="token" type="hidden" id="token" value="{{{token}}}" />
|
||||
<input name="locale" type="hidden" id="locale" value="{{{locale}}}" />
|
||||
|
||||
<p>New Password</p>
|
||||
<input name="new_password" type="password" id="password" />
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
<body>
|
||||
<h1>{{appName}}</h1>
|
||||
<h1>Expired verification link!</h1>
|
||||
<form method="POST" action="{{publicServerUrl}}/apps/{{appId}}/resend_verification_email">
|
||||
<input name="token" type="hidden" value="{{token}}">
|
||||
<input name="locale" type="hidden" value="{{locale}}">
|
||||
<form method="POST" action="{{{publicServerUrl}}}/apps/{{{appId}}}/resend_verification_email">
|
||||
<input name="username" type="hidden" value="{{{username}}}">
|
||||
<input name="locale" type="hidden" value="{{{locale}}}">
|
||||
<button type="submit">Resend Link</button>
|
||||
</form>
|
||||
</body>
|
||||
|
||||
@@ -23,11 +23,11 @@
|
||||
<p>You can set a new Password for your account: {{username}}</p>
|
||||
<br />
|
||||
<p>{{error}}</p>
|
||||
<form id='form' action='{{publicServerUrl}}/apps/{{appId}}/request_password_reset' method='POST'>
|
||||
<form id='form' action='{{{publicServerUrl}}}/apps/{{{appId}}}/request_password_reset' method='POST'>
|
||||
<input name='utf-8' type='hidden' value='✓' />
|
||||
<input name="username" type="hidden" id="username" value="{{username}}" />
|
||||
<input name="token" type="hidden" id="token" value="{{token}}" />
|
||||
<input name="locale" type="hidden" id="locale" value="{{locale}}" />
|
||||
<input name="username" type="hidden" id="username" value="{{{username}}}" />
|
||||
<input name="token" type="hidden" id="token" value="{{{token}}}" />
|
||||
<input name="locale" type="hidden" id="locale" value="{{{locale}}}" />
|
||||
|
||||
<p>New Password</p>
|
||||
<input name="new_password" type="password" id="password" />
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
<body>
|
||||
<h1>{{appName}}</h1>
|
||||
<h1>Expired verification link!</h1>
|
||||
<form method="POST" action="{{publicServerUrl}}/apps/{{appId}}/resend_verification_email">
|
||||
<input name="token" type="hidden" value="{{token}}">
|
||||
<input name="locale" type="hidden" value="{{locale}}">
|
||||
<form method="POST" action="{{{publicServerUrl}}}/apps/{{{appId}}}/resend_verification_email">
|
||||
<input name="username" type="hidden" value="{{{username}}}">
|
||||
<input name="locale" type="hidden" value="{{{locale}}}">
|
||||
<button type="submit">Resend Link</button>
|
||||
</form>
|
||||
</body>
|
||||
|
||||
@@ -23,11 +23,11 @@
|
||||
<p>You can set a new Password for your account: {{username}}</p>
|
||||
<br />
|
||||
<p>{{error}}</p>
|
||||
<form id='form' action='{{publicServerUrl}}/apps/{{appId}}/request_password_reset' method='POST'>
|
||||
<form id='form' action='{{{publicServerUrl}}}/apps/{{{appId}}}/request_password_reset' method='POST'>
|
||||
<input name='utf-8' type='hidden' value='✓' />
|
||||
<input name="username" type="hidden" id="username" value="{{username}}" />
|
||||
<input name="token" type="hidden" id="token" value="{{token}}" />
|
||||
<input name="locale" type="hidden" id="locale" value="{{locale}}" />
|
||||
<input name="username" type="hidden" id="username" value="{{{username}}}" />
|
||||
<input name="token" type="hidden" id="token" value="{{{token}}}" />
|
||||
<input name="locale" type="hidden" id="locale" value="{{{locale}}}" />
|
||||
|
||||
<p>New Password</p>
|
||||
<input name="new_password" type="password" id="password" />
|
||||
|
||||
@@ -47,8 +47,8 @@
|
||||
window.onload = addDataToForm;
|
||||
|
||||
function addDataToForm() {
|
||||
const token = getUrlParameter("token");
|
||||
document.getElementById("token").value = token;
|
||||
var username = getUrlParameter("username");
|
||||
document.getElementById("usernameField").value = username;
|
||||
|
||||
var appId = getUrlParameter("appId");
|
||||
document.getElementById("resendForm").action = '/apps/' + appId + '/resend_verification_email'
|
||||
@@ -60,7 +60,7 @@
|
||||
<div class="container">
|
||||
<h1>Invalid Verification Link</h1>
|
||||
<form id="resendForm" method="POST" action="/resend_verification_email">
|
||||
<input id="token" class="form-control" name="token" type="hidden" value="">
|
||||
<input id="usernameField" class="form-control" name="username" type="hidden" value="">
|
||||
<button type="submit" class="btn btn-default">Resend Link</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -2,13 +2,8 @@
|
||||
* Semantic Release Config
|
||||
*/
|
||||
|
||||
const { readFile } = require('fs').promises;
|
||||
const { resolve } = require('path');
|
||||
|
||||
// For ES6 modules use:
|
||||
// import { readFile } from 'fs/promises';
|
||||
// import { resolve, dirname } from 'path';
|
||||
// import { fileURLToPath } from 'url';
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
// Get env vars
|
||||
const ref = process.env.GITHUB_REF;
|
||||
@@ -29,13 +24,11 @@ const templates = {
|
||||
async function config() {
|
||||
|
||||
// Get branch
|
||||
const branch = ref?.split('/')?.pop()?.split('-')[0] || '(current branch could not be determined)';
|
||||
// eslint-disable-next-line no-console
|
||||
const branch = ref.split('/').pop();
|
||||
console.log(`Running on branch: ${branch}`);
|
||||
|
||||
// Set changelog file
|
||||
const changelogFile = `./changelogs/CHANGELOG_release.md`;
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Changelog file output to: ${changelogFile}`);
|
||||
|
||||
// Load template file contents
|
||||
@@ -45,9 +38,9 @@ async function config() {
|
||||
branches: [
|
||||
'release',
|
||||
{ name: 'alpha', prerelease: true },
|
||||
// { name: 'beta', prerelease: true },
|
||||
// Long-Term-Support branch
|
||||
{ name: 'release-8.x.x', range: '8.x.x', channel: '8.x.x' },
|
||||
{ name: 'beta', prerelease: true },
|
||||
// Long-Term-Support branches
|
||||
{ name: 'release-6.x.x', range: '6.x.x', channel: '6.x.x' },
|
||||
],
|
||||
dryRun: false,
|
||||
debug: true,
|
||||
@@ -61,13 +54,13 @@ async function config() {
|
||||
{ scope: 'no-release', release: false },
|
||||
],
|
||||
parserOpts: {
|
||||
noteKeywords: ['BREAKING CHANGE'],
|
||||
noteKeywords: [ 'BREAKING CHANGE', 'BREAKING CHANGES', 'BREAKING' ],
|
||||
},
|
||||
}],
|
||||
['@semantic-release/release-notes-generator', {
|
||||
preset: 'angular',
|
||||
parserOpts: {
|
||||
noteKeywords: ['BREAKING CHANGE']
|
||||
noteKeywords: ['BREAKING CHANGE', 'BREAKING CHANGES', 'BREAKING']
|
||||
},
|
||||
writerOpts: {
|
||||
commitsSort: ['subject', 'scope'],
|
||||
@@ -91,17 +84,6 @@ async function config() {
|
||||
labels: ['type:ci'],
|
||||
releasedLabels: ['state:released<%= nextRelease.channel ? `-\${nextRelease.channel}` : "" %>']
|
||||
}],
|
||||
// Back-merge module runs last because if it fails it should not impede the release process
|
||||
[
|
||||
"@saithodev/semantic-release-backmerge",
|
||||
{
|
||||
"backmergeBranches": [
|
||||
// { from: 'beta', to: 'alpha' },
|
||||
// { from: 'release', to: 'beta' },
|
||||
{ from: 'release', to: 'alpha' },
|
||||
]
|
||||
}
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
@@ -110,17 +92,15 @@ async function config() {
|
||||
|
||||
async function loadTemplates() {
|
||||
for (const template of Object.keys(templates)) {
|
||||
|
||||
// For ES6 modules use:
|
||||
// const fileUrl = import.meta.url;
|
||||
// const __dirname = dirname(fileURLToPath(fileUrl));
|
||||
|
||||
const filePath = resolve(__dirname, resourcePath, templates[template].file);
|
||||
const text = await readFile(filePath, 'utf-8');
|
||||
const text = await readFile(path.resolve(__dirname, resourcePath, templates[template].file));
|
||||
templates[template].text = text;
|
||||
}
|
||||
}
|
||||
|
||||
async function readFile(filePath) {
|
||||
return await fs.readFile(filePath, 'utf-8');
|
||||
}
|
||||
|
||||
function getReleaseComment() {
|
||||
const url = repositoryUrl + '/releases/tag/${nextRelease.gitTag}';
|
||||
const comment = '🎉 This change has been released in version [${nextRelease.version}](' + url + ')';
|
||||
+9
-16
@@ -1,17 +1,10 @@
|
||||
#!/bin/sh -e
|
||||
set -x
|
||||
# GITHUB_ACTIONS=true SOURCE_TAG=test ./release_docs.sh
|
||||
|
||||
if [ "${GITHUB_ACTIONS}" = "" ];
|
||||
then
|
||||
echo "Cannot release docs without GITHUB_ACTIONS set"
|
||||
exit 0;
|
||||
fi
|
||||
if [ "${SOURCE_TAG}" = "" ];
|
||||
then
|
||||
echo "Cannot release docs without SOURCE_TAG set"
|
||||
exit 0;
|
||||
fi
|
||||
REPO="https://github.com/parse-community/parse-server"
|
||||
|
||||
rm -rf docs
|
||||
@@ -20,20 +13,20 @@ cd docs
|
||||
git pull origin gh-pages
|
||||
cd ..
|
||||
|
||||
RELEASE="release"
|
||||
VERSION="${SOURCE_TAG}"
|
||||
DEST="master"
|
||||
|
||||
# change the default page to the latest
|
||||
echo "<meta http-equiv='refresh' content='0; url=/parse-server/api/${VERSION}'>" > "docs/api/index.html"
|
||||
if [ "${SOURCE_TAG}" != "" ];
|
||||
then
|
||||
DEST="${SOURCE_TAG}"
|
||||
# change the default page to the latest
|
||||
echo "<meta http-equiv='refresh' content='0; url=/parse-server/api/${DEST}'>" > "docs/api/index.html"
|
||||
fi
|
||||
|
||||
npm run definitions
|
||||
npm run docs
|
||||
|
||||
mkdir -p "docs/api/${RELEASE}"
|
||||
cp -R out/* "docs/api/${RELEASE}"
|
||||
|
||||
mkdir -p "docs/api/${VERSION}"
|
||||
cp -R out/* "docs/api/${VERSION}"
|
||||
mkdir -p "docs/api/${DEST}"
|
||||
cp -R out/* "docs/api/${DEST}"
|
||||
|
||||
# Copy other resources
|
||||
RESOURCE_DIR=".github"
|
||||
|
||||
@@ -22,7 +22,6 @@ const nestedOptionTypes = [
|
||||
'PagesOptions',
|
||||
'PagesRoute',
|
||||
'PasswordPolicyOptions',
|
||||
'RequestComplexityOptions',
|
||||
'SecurityOptions',
|
||||
'SchemaOptions',
|
||||
'LogLevels',
|
||||
@@ -37,18 +36,15 @@ const nestedOptionEnvPrefix = {
|
||||
IdempotencyOptions: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_',
|
||||
LiveQueryOptions: 'PARSE_SERVER_LIVEQUERY_',
|
||||
LiveQueryServerOptions: 'PARSE_LIVE_QUERY_SERVER_',
|
||||
LogClientEvent: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS_',
|
||||
LogLevel: 'PARSE_SERVER_LOG_LEVEL_',
|
||||
LogLevels: 'PARSE_SERVER_LOG_LEVELS_',
|
||||
PagesCustomUrlsOptions: 'PARSE_SERVER_PAGES_CUSTOM_URL_',
|
||||
PagesOptions: 'PARSE_SERVER_PAGES_',
|
||||
PagesRoute: 'PARSE_SERVER_PAGES_ROUTE_',
|
||||
ParseServerOptions: 'PARSE_SERVER_',
|
||||
PasswordPolicyOptions: 'PARSE_SERVER_PASSWORD_POLICY_',
|
||||
RateLimitOptions: 'PARSE_SERVER_RATE_LIMIT_',
|
||||
RequestComplexityOptions: 'PARSE_SERVER_REQUEST_COMPLEXITY_',
|
||||
SchemaOptions: 'PARSE_SERVER_SCHEMA_',
|
||||
SecurityOptions: 'PARSE_SERVER_SECURITY_',
|
||||
SchemaOptions: 'PARSE_SERVER_SCHEMA_',
|
||||
LogLevels: 'PARSE_SERVER_LOG_LEVELS_',
|
||||
RateLimitOptions: 'PARSE_SERVER_RATE_LIMIT_',
|
||||
};
|
||||
|
||||
function last(array) {
|
||||
@@ -157,8 +153,6 @@ function mapperFor(elt, t) {
|
||||
return wrap(t.identifier('objectParser'));
|
||||
} else if (t.isBooleanTypeAnnotation(elt)) {
|
||||
return wrap(t.identifier('booleanParser'));
|
||||
} else if (t.isObjectTypeAnnotation(elt)) {
|
||||
return wrap(t.identifier('objectParser'));
|
||||
} else if (t.isGenericTypeAnnotation(elt)) {
|
||||
const type = elt.typeAnnotation.id.name;
|
||||
if (type == 'Adapter') {
|
||||
@@ -167,9 +161,6 @@ function mapperFor(elt, t) {
|
||||
if (type == 'NumberOrBoolean') {
|
||||
return wrap(t.identifier('numberOrBooleanParser'));
|
||||
}
|
||||
if (type == 'NumberOrString') {
|
||||
return t.callExpression(wrap(t.identifier('numberOrStringParser')), [t.stringLiteral(elt.name)]);
|
||||
}
|
||||
if (type === 'StringOrStringArray') {
|
||||
return wrap(t.identifier('arrayParser'));
|
||||
}
|
||||
@@ -221,9 +212,6 @@ function parseDefaultValue(elt, value, t) {
|
||||
if (type == 'NumberOrBoolean') {
|
||||
literalValue = t.numericLiteral(parsers.numberOrBoolParser('')(value));
|
||||
}
|
||||
if (type == 'NumberOrString') {
|
||||
literalValue = t.numericLiteral(parsers.numberOrStringParser('')(value));
|
||||
}
|
||||
|
||||
if (nestedOptionTypes.includes(type)) {
|
||||
const object = parsers.objectParser(value);
|
||||
@@ -266,23 +254,6 @@ function inject(t, list) {
|
||||
if (action) {
|
||||
props.push(t.objectProperty(t.stringLiteral('action'), action));
|
||||
}
|
||||
|
||||
if (t.isGenericTypeAnnotation(elt)) {
|
||||
if (elt.typeAnnotation.id.name in nestedOptionEnvPrefix) {
|
||||
props.push(
|
||||
t.objectProperty(t.stringLiteral('type'), t.stringLiteral(elt.typeAnnotation.id.name))
|
||||
);
|
||||
}
|
||||
} else if (t.isArrayTypeAnnotation(elt)) {
|
||||
const elementType = elt.typeAnnotation.elementType;
|
||||
if (t.isGenericTypeAnnotation(elementType)) {
|
||||
if (elementType.id.name in nestedOptionEnvPrefix) {
|
||||
props.push(
|
||||
t.objectProperty(t.stringLiteral('type'), t.stringLiteral(elementType.id.name + '[]'))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (elt.defaultValue) {
|
||||
let parsedValue = parseDefaultValue(elt, elt.defaultValue, t);
|
||||
if (!parsedValue) {
|
||||
@@ -376,18 +347,12 @@ This code has been generated by resources/buildConfigDefinitions.js
|
||||
Do not edit manually, but update Options/index.js
|
||||
`;
|
||||
|
||||
// Only run the transformation when executed directly, not when imported by tests
|
||||
if (require.main === module) {
|
||||
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);
|
||||
}
|
||||
|
||||
// Export mapperFor for testing
|
||||
module.exports = { mapperFor };
|
||||
const babel = require('@babel/core');
|
||||
const res = babel.transformFileSync('./src/Options/index.js', {
|
||||
plugins: [plugin, '@babel/transform-flow-strip-types'],
|
||||
babelrc: false,
|
||||
auxiliaryCommentBefore,
|
||||
sourceMaps: false,
|
||||
});
|
||||
require('fs').writeFileSync('./src/Options/Definitions.js', res.code + '\n');
|
||||
require('fs').writeFileSync('./src/Options/docs.js', docs);
|
||||
|
||||
+3
-3
@@ -3,12 +3,12 @@
|
||||
"@babel/plugin-proposal-object-rest-spread"
|
||||
],
|
||||
"presets": [
|
||||
"@babel/preset-typescript",
|
||||
["@babel/preset-env", {
|
||||
"targets": {
|
||||
"node": "18"
|
||||
"node": "14"
|
||||
}
|
||||
}]
|
||||
],
|
||||
"sourceMaps": "inline"
|
||||
"sourceMaps": "inline",
|
||||
"retainLines": true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"env": {
|
||||
"jasmine": true
|
||||
},
|
||||
"globals": {
|
||||
"Parse": true,
|
||||
"reconfigureServer": true,
|
||||
"createTestUser": true,
|
||||
"jfail": true,
|
||||
"ok": true,
|
||||
"strictEqual": true,
|
||||
"TestObject": true,
|
||||
"Item": true,
|
||||
"Container": true,
|
||||
"equal": true,
|
||||
"expectAsync": true,
|
||||
"notEqual": true,
|
||||
"it_id": true,
|
||||
"it_only_db": true,
|
||||
"it_only_mongodb_version": true,
|
||||
"it_only_postgres_version": true,
|
||||
"it_only_node_version": true,
|
||||
"fit_only_mongodb_version": true,
|
||||
"fit_only_node_version": true,
|
||||
"it_exclude_mongodb_version": true,
|
||||
"it_exclude_postgres_version": true,
|
||||
"fit_exclude_mongodb_version": true,
|
||||
"fit_exclude_node_version": true,
|
||||
"it_exclude_dbs": true,
|
||||
"describe_only_db": true,
|
||||
"describe_only": true,
|
||||
"on_db": true,
|
||||
"defaultConfiguration": true,
|
||||
"range": true,
|
||||
"jequal": true,
|
||||
"create": true,
|
||||
"arrayContains": true,
|
||||
"databaseAdapter": true,
|
||||
"databaseURI": true
|
||||
},
|
||||
"rules": {
|
||||
"no-console": [0],
|
||||
"no-var": "error"
|
||||
}
|
||||
}
|
||||
@@ -419,7 +419,7 @@ describe('lockout with password reset option', () => {
|
||||
await request({
|
||||
method: 'POST',
|
||||
url: `${config.publicServerURL}/apps/test/request_password_reset`,
|
||||
body: `new_password=${newPassword}&token=${token}`,
|
||||
body: `new_password=${newPassword}&token=${token}&username=${username}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
@@ -454,7 +454,7 @@ describe('lockout with password reset option', () => {
|
||||
await request({
|
||||
method: 'POST',
|
||||
url: `${config.publicServerURL}/apps/test/request_password_reset`,
|
||||
body: `new_password=${newPassword}&token=${token}`,
|
||||
body: `new_password=${newPassword}&token=${token}&username=${username}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const { loadAdapter, loadModule } = require('../lib/Adapters/AdapterLoader');
|
||||
const loadAdapter = require('../lib/Adapters/AdapterLoader').loadAdapter;
|
||||
const FilesAdapter = require('@parse/fs-files-adapter').default;
|
||||
const ParsePushAdapter = require('@parse/push-adapter').default;
|
||||
const MockFilesAdapter = require('mock-files-adapter');
|
||||
const Config = require('../lib/Config');
|
||||
|
||||
@@ -102,19 +103,19 @@ describe('AdapterLoader', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should load push adapter from options', async () => {
|
||||
it('should load push adapter from options', done => {
|
||||
const options = {
|
||||
android: {
|
||||
senderId: 'yolo',
|
||||
apiKey: 'yolo',
|
||||
},
|
||||
};
|
||||
const ParsePushAdapter = await loadModule('@parse/push-adapter');
|
||||
expect(() => {
|
||||
const adapter = loadAdapter(undefined, ParsePushAdapter, options);
|
||||
expect(adapter.constructor).toBe(ParsePushAdapter);
|
||||
expect(adapter).not.toBe(undefined);
|
||||
}).not.toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('should load custom push adapter from string (#3544)', done => {
|
||||
@@ -141,25 +142,6 @@ describe('AdapterLoader', () => {
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should load custom database adapter from config', done => {
|
||||
const adapterPath = require('path').resolve('./spec/support/MockDatabaseAdapter');
|
||||
const options = {
|
||||
databaseURI: 'oracledb://user:password@localhost:1521/freepdb1',
|
||||
collectionPrefix: '',
|
||||
};
|
||||
const databaseAdapterOptions = {
|
||||
adapter: adapterPath,
|
||||
options,
|
||||
};
|
||||
expect(() => {
|
||||
const databaseAdapter = loadAdapter(databaseAdapterOptions);
|
||||
expect(databaseAdapter).not.toBe(undefined);
|
||||
expect(databaseAdapter.options).toEqual(options);
|
||||
expect(databaseAdapter.getDatabaseURI()).toEqual(options.databaseURI);
|
||||
}).not.toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('should load file adapter from direct passing', done => {
|
||||
spyOn(console, 'warn').and.callFake(() => {});
|
||||
const mockFilesAdapter = new MockFilesAdapter('key', 'secret', 'bucket');
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
const BaseAuthCodeAdapter = require('../../../lib/Adapters/Auth/BaseCodeAuthAdapter').default;
|
||||
|
||||
describe('BaseAuthCodeAdapter', function () {
|
||||
let adapter;
|
||||
const adapterName = 'TestAdapter';
|
||||
const validOptions = {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
};
|
||||
|
||||
class TestAuthCodeAdapter extends BaseAuthCodeAdapter {
|
||||
async getUserFromAccessToken(accessToken) {
|
||||
if (accessToken === 'validAccessToken') {
|
||||
return { id: 'validUserId' };
|
||||
}
|
||||
throw new Error('Invalid access token');
|
||||
}
|
||||
|
||||
async getAccessTokenFromCode(authData) {
|
||||
if (authData.code === 'validCode') {
|
||||
return 'validAccessToken';
|
||||
}
|
||||
throw new Error('Invalid code');
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new TestAuthCodeAdapter(adapterName);
|
||||
});
|
||||
|
||||
describe('validateOptions', function () {
|
||||
it('should throw error if options are missing', function () {
|
||||
expect(() => adapter.validateOptions(null)).toThrowError(`${adapterName} options are required.`);
|
||||
});
|
||||
|
||||
it('should throw error if clientId is missing in secure mode', function () {
|
||||
expect(() =>
|
||||
adapter.validateOptions({ clientSecret: 'validClientSecret' })
|
||||
).toThrowError(`${adapterName} clientId is required.`);
|
||||
});
|
||||
|
||||
it('should throw error if clientSecret is missing in secure mode', function () {
|
||||
expect(() =>
|
||||
adapter.validateOptions({ clientId: 'validClientId' })
|
||||
).toThrowError(`${adapterName} clientSecret is required.`);
|
||||
});
|
||||
|
||||
it('should not throw error for valid options', function () {
|
||||
expect(() => adapter.validateOptions(validOptions)).not.toThrow();
|
||||
expect(adapter.clientId).toBe('validClientId');
|
||||
expect(adapter.clientSecret).toBe('validClientSecret');
|
||||
expect(adapter.enableInsecureAuth).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should allow insecure mode without clientId or clientSecret', function () {
|
||||
const options = { enableInsecureAuth: true };
|
||||
expect(() => adapter.validateOptions(options)).not.toThrow();
|
||||
expect(adapter.enableInsecureAuth).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('beforeFind', function () {
|
||||
it('should throw error if code is missing in secure mode', async function () {
|
||||
adapter.validateOptions(validOptions);
|
||||
const authData = { access_token: 'validAccessToken' };
|
||||
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeRejectedWithError(
|
||||
`${adapterName} code is required.`
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error if access token is missing in insecure mode', async function () {
|
||||
adapter.validateOptions({ enableInsecureAuth: true });
|
||||
const authData = {};
|
||||
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeRejectedWithError(
|
||||
`${adapterName} auth is invalid for this user.`
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error if user ID does not match in insecure mode', async function () {
|
||||
adapter.validateOptions({ enableInsecureAuth: true });
|
||||
const authData = { id: 'invalidUserId', access_token: 'validAccessToken' };
|
||||
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeRejectedWithError(
|
||||
`${adapterName} auth is invalid for this user.`
|
||||
);
|
||||
});
|
||||
|
||||
it('should process valid secure payload and update authData', async function () {
|
||||
adapter.validateOptions(validOptions);
|
||||
const authData = { code: 'validCode' };
|
||||
|
||||
await adapter.beforeFind(authData);
|
||||
|
||||
expect(authData.access_token).toBe('validAccessToken');
|
||||
expect(authData.id).toBe('validUserId');
|
||||
expect(authData.code).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should process valid insecure payload', async function () {
|
||||
adapter.validateOptions({ enableInsecureAuth: true });
|
||||
const authData = { id: 'validUserId', access_token: 'validAccessToken' };
|
||||
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeResolved();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserFromAccessToken', function () {
|
||||
it('should throw error if not implemented in base class', async function () {
|
||||
const baseAdapter = new BaseAuthCodeAdapter(adapterName);
|
||||
|
||||
await expectAsync(baseAdapter.getUserFromAccessToken('test')).toBeRejectedWithError(
|
||||
'getUserFromAccessToken is not implemented'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return valid user for valid access token', async function () {
|
||||
const user = await adapter.getUserFromAccessToken('validAccessToken', {});
|
||||
expect(user).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
|
||||
it('should throw error for invalid access token', async function () {
|
||||
await expectAsync(adapter.getUserFromAccessToken('invalidAccessToken', {})).toBeRejectedWithError(
|
||||
'Invalid access token'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAccessTokenFromCode', function () {
|
||||
it('should throw error if not implemented in base class', async function () {
|
||||
const baseAdapter = new BaseAuthCodeAdapter(adapterName);
|
||||
|
||||
await expectAsync(baseAdapter.getAccessTokenFromCode({ code: 'test' })).toBeRejectedWithError(
|
||||
'getAccessTokenFromCode is not implemented'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return valid access token for valid code', async function () {
|
||||
const accessToken = await adapter.getAccessTokenFromCode({ code: 'validCode' });
|
||||
expect(accessToken).toBe('validAccessToken');
|
||||
});
|
||||
|
||||
it('should throw error for invalid code', async function () {
|
||||
await expectAsync(adapter.getAccessTokenFromCode({ code: 'invalidCode' })).toBeRejectedWithError(
|
||||
'Invalid code'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateLogin', function () {
|
||||
it('should return user id from authData', function () {
|
||||
const authData = { id: 'validUserId' };
|
||||
const result = adapter.validateLogin(authData);
|
||||
expect(result).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateSetUp', function () {
|
||||
it('should return user id from authData', function () {
|
||||
const authData = { id: 'validUserId' };
|
||||
const result = adapter.validateSetUp(authData);
|
||||
expect(result).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('afterFind', function () {
|
||||
it('should return user id from authData', function () {
|
||||
const authData = { id: 'validUserId' };
|
||||
const result = adapter.afterFind(authData);
|
||||
expect(result).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateUpdate', function () {
|
||||
it('should return user id from authData', function () {
|
||||
const authData = { id: 'validUserId' };
|
||||
const result = adapter.validateUpdate(authData);
|
||||
expect(result).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,220 +0,0 @@
|
||||
const GameCenterAuth = require('../../../lib/Adapters/Auth/gcenter').default;
|
||||
const { pki } = require('node-forge');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
describe('GameCenterAuth Adapter', function () {
|
||||
let adapter;
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new GameCenterAuth.constructor();
|
||||
|
||||
const gcProd4 = fs.readFileSync(path.resolve(__dirname, '../../support/cert/gc-prod-4.cer'));
|
||||
const digicertPem = fs.readFileSync(path.resolve(__dirname, '../../support/cert/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem')).toString();
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
headers: new Map(),
|
||||
arrayBuffer: () => Promise.resolve(
|
||||
gcProd4.buffer.slice(gcProd4.byteOffset, gcProd4.byteOffset + gcProd4.length)
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
headers: new Map([['content-type', 'application/x-pem-file'], ['content-length', digicertPem.length.toString()]]),
|
||||
text: () => Promise.resolve(digicertPem),
|
||||
},
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
describe('Test config failing due to missing params or wrong types', function () {
|
||||
it('should throw error for invalid options', async function () {
|
||||
const invalidOptions = [
|
||||
null,
|
||||
undefined,
|
||||
{},
|
||||
{ bundleId: '' },
|
||||
{ enableInsecureAuth: false }, // Missing bundleId in secure mode
|
||||
];
|
||||
|
||||
for (const options of invalidOptions) {
|
||||
expect(() => adapter.validateOptions(options)).withContext(JSON.stringify(options)).toThrow()
|
||||
}
|
||||
});
|
||||
|
||||
it('should validate options successfully with valid parameters', function () {
|
||||
const validOptions = { bundleId: 'com.valid.app', enableInsecureAuth: false };
|
||||
expect(() => adapter.validateOptions(validOptions)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test payload failing due to missing params or wrong types', function () {
|
||||
it('should throw error for missing authData fields', async function () {
|
||||
await expectAsync(adapter.validateAuthData({})).toBeRejectedWithError(
|
||||
'AuthData id is missing.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test payload fails due to incorrect appId / certificate', function () {
|
||||
it('should throw error for invalid publicKeyUrl', async function () {
|
||||
const invalidPublicKeyUrl = 'https://malicious.url.com/key.cer';
|
||||
|
||||
spyOn(adapter, 'fetchCertificate').and.throwError(
|
||||
new Error('Invalid publicKeyUrl')
|
||||
);
|
||||
|
||||
await expectAsync(
|
||||
adapter.getAppleCertificate(invalidPublicKeyUrl)
|
||||
).toBeRejectedWithError('Invalid publicKeyUrl: https://malicious.url.com/key.cer');
|
||||
});
|
||||
|
||||
it('should throw error for invalid signature verification', async function () {
|
||||
const fakePublicKey = 'invalid-key';
|
||||
const fakeAuthData = {
|
||||
id: '1234567',
|
||||
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||
timestamp: 1460981421303,
|
||||
salt: 'saltST==',
|
||||
signature: 'invalidSignature',
|
||||
};
|
||||
|
||||
spyOn(adapter, 'getAppleCertificate').and.returnValue(Promise.resolve(fakePublicKey));
|
||||
spyOn(adapter, 'verifySignature').and.throwError('Invalid signature.');
|
||||
|
||||
await expectAsync(adapter.validateAuthData(fakeAuthData)).toBeRejectedWithError(
|
||||
'Invalid signature.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test payload passing', function () {
|
||||
it('should successfully process valid payload and save auth data', async function () {
|
||||
const validAuthData = {
|
||||
id: '1234567',
|
||||
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||
timestamp: 1460981421303,
|
||||
salt: 'saltST==',
|
||||
signature: 'validSignature',
|
||||
bundleId: 'com.valid.app',
|
||||
};
|
||||
|
||||
spyOn(adapter, 'getAppleCertificate').and.returnValue(Promise.resolve('validKey'));
|
||||
spyOn(adapter, 'verifySignature').and.returnValue(true);
|
||||
|
||||
await expectAsync(adapter.validateAuthData(validAuthData)).toBeResolved();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Certificate and Signature Validation', function () {
|
||||
it('should fetch and validate Apple certificate', async function () {
|
||||
const certUrl = 'https://static.gc.apple.com/public-key/gc-prod-4.cer';
|
||||
const mockCertificate = 'mockCertificate';
|
||||
|
||||
spyOn(adapter, 'fetchCertificate').and.returnValue(
|
||||
Promise.resolve({ certificate: mockCertificate, headers: new Map() })
|
||||
);
|
||||
spyOn(pki, 'certificateFromPem').and.returnValue({});
|
||||
|
||||
adapter.cache[certUrl] = mockCertificate;
|
||||
|
||||
const cert = await adapter.getAppleCertificate(certUrl);
|
||||
expect(cert).toBe(mockCertificate);
|
||||
});
|
||||
|
||||
it('should verify signature successfully', async function () {
|
||||
const authData = {
|
||||
id: 'G:1965586982',
|
||||
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||
timestamp: 1565257031287,
|
||||
signature:
|
||||
'uqLBTr9Uex8zCpc1UQ1MIDMitb+HUat2Mah4Kw6AVLSGe0gGNJXlih2i5X+0ZwVY0S9zY2NHWi2gFjmhjt/4kxWGMkupqXX5H/qhE2m7hzox6lZJpH98ZEUbouWRfZX2ZhUlCkAX09oRNi7fI7mWL1/o88MaI/y6k6tLr14JTzmlxgdyhw+QRLxRPA6NuvUlRSJpyJ4aGtNH5/wHdKQWL8nUnFYiYmaY8R7IjzNxPfy8UJTUWmeZvMSgND4u8EjADPsz7ZtZyWAPi8kYcAb6M8k0jwLD3vrYCB8XXyO2RQb/FY2TM4zJuI7PzLlvvgOJXbbfVtHx7Evnm5NYoyzgzw==',
|
||||
salt: 'DzqqrQ==',
|
||||
};
|
||||
|
||||
adapter.bundleId = 'cloud.xtralife.gamecenterauth';
|
||||
adapter.enableInsecureAuth = false;
|
||||
|
||||
spyOn(adapter, 'verifyPublicKeyIssuer').and.returnValue();
|
||||
|
||||
const publicKey = await adapter.getAppleCertificate(authData.publicKeyUrl);
|
||||
|
||||
expect(() => adapter.verifySignature(publicKey, authData)).not.toThrow();
|
||||
|
||||
});
|
||||
|
||||
it('should not use bundle id from authData payload in secure mode', async function () {
|
||||
const authData = {
|
||||
id: 'G:1965586982',
|
||||
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||
timestamp: 1565257031287,
|
||||
signature:
|
||||
'uqLBTr9Uex8zCpc1UQ1MIDMitb+HUat2Mah4Kw6AVLSGe0gGNJXlih2i5X+0ZwVY0S9zY2NHWi2gFjmhjt/4kxWGMkupqXX5H/qhE2m7hzox6lZJpH98ZEUbouWRfZX2ZhUlCkAX09oRNi7fI7mWL1/o88MaI/y6k6tLr14JTzmlxgdyhw+QRLxRPA6NuvUlRSJpyJ4aGtNH5/wHdKQWL8nUnFYiYmaY8R7IjzNxPfy8UJTUWmeZvMSgND4u8EjADPsz7ZtZyWAPi8kYcAb6M8k0jwLD3vrYCB8XXyO2RQb/FY2TM4zJuI7PzLlvvgOJXbbfVtHx7Evnm5NYoyzgzw==',
|
||||
salt: 'DzqqrQ==',
|
||||
bundleId: 'com.example.insecure.app',
|
||||
};
|
||||
|
||||
adapter.bundleId = 'cloud.xtralife.gamecenterauth';
|
||||
adapter.enableInsecureAuth = false;
|
||||
|
||||
spyOn(adapter, 'verifyPublicKeyIssuer').and.returnValue();
|
||||
|
||||
const publicKey = await adapter.getAppleCertificate(authData.publicKeyUrl);
|
||||
|
||||
expect(() => adapter.verifySignature(publicKey, authData)).not.toThrow();
|
||||
|
||||
});
|
||||
|
||||
it('should not use bundle id from authData payload in insecure mode', async function () {
|
||||
const authData = {
|
||||
id: 'G:1965586982',
|
||||
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||
timestamp: 1565257031287,
|
||||
signature:
|
||||
'uqLBTr9Uex8zCpc1UQ1MIDMitb+HUat2Mah4Kw6AVLSGe0gGNJXlih2i5X+0ZwVY0S9zY2NHWi2gFjmhjt/4kxWGMkupqXX5H/qhE2m7hzox6lZJpH98ZEUbouWRfZX2ZhUlCkAX09oRNi7fI7mWL1/o88MaI/y6k6tLr14JTzmlxgdyhw+QRLxRPA6NuvUlRSJpyJ4aGtNH5/wHdKQWL8nUnFYiYmaY8R7IjzNxPfy8UJTUWmeZvMSgND4u8EjADPsz7ZtZyWAPi8kYcAb6M8k0jwLD3vrYCB8XXyO2RQb/FY2TM4zJuI7PzLlvvgOJXbbfVtHx7Evnm5NYoyzgzw==',
|
||||
salt: 'DzqqrQ==',
|
||||
bundleId: 'com.example.insecure.app',
|
||||
};
|
||||
|
||||
adapter.bundleId = 'cloud.xtralife.gamecenterauth';
|
||||
adapter.enableInsecureAuth = true;
|
||||
|
||||
spyOn(adapter, 'verifyPublicKeyIssuer').and.returnValue();
|
||||
|
||||
const publicKey = await adapter.getAppleCertificate(authData.publicKeyUrl);
|
||||
|
||||
expect(() => adapter.verifySignature(publicKey, authData)).not.toThrow();
|
||||
|
||||
});
|
||||
|
||||
it('can use bundle id from authData payload in insecure mode', async function () {
|
||||
const authData = {
|
||||
id: 'G:1965586982',
|
||||
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||
timestamp: 1565257031287,
|
||||
signature:
|
||||
'uqLBTr9Uex8zCpc1UQ1MIDMitb+HUat2Mah4Kw6AVLSGe0gGNJXlih2i5X+0ZwVY0S9zY2NHWi2gFjmhjt/4kxWGMkupqXX5H/qhE2m7hzox6lZJpH98ZEUbouWRfZX2ZhUlCkAX09oRNi7fI7mWL1/o88MaI/y6k6tLr14JTzmlxgdyhw+QRLxRPA6NuvUlRSJpyJ4aGtNH5/wHdKQWL8nUnFYiYmaY8R7IjzNxPfy8UJTUWmeZvMSgND4u8EjADPsz7ZtZyWAPi8kYcAb6M8k0jwLD3vrYCB8XXyO2RQb/FY2TM4zJuI7PzLlvvgOJXbbfVtHx7Evnm5NYoyzgzw==',
|
||||
salt: 'DzqqrQ==',
|
||||
bundleId: 'cloud.xtralife.gamecenterauth',
|
||||
};
|
||||
|
||||
adapter.enableInsecureAuth = true;
|
||||
|
||||
spyOn(adapter, 'verifyPublicKeyIssuer').and.returnValue();
|
||||
|
||||
const publicKey = await adapter.getAppleCertificate(authData.publicKeyUrl);
|
||||
|
||||
expect(() => adapter.verifySignature(publicKey, authData)).not.toThrow();
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,285 +0,0 @@
|
||||
const GitHubAdapter = require('../../../lib/Adapters/Auth/github').default;
|
||||
|
||||
describe('GitHubAdapter', function () {
|
||||
let adapter;
|
||||
const validOptions = {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new GitHubAdapter.constructor();
|
||||
adapter.validateOptions(validOptions);
|
||||
});
|
||||
|
||||
describe('getAccessTokenFromCode', function () {
|
||||
it('should fetch an access token successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://github.com/login/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const code = 'validCode';
|
||||
const token = await adapter.getAccessTokenFromCode(code);
|
||||
|
||||
expect(token).toBe('mockAccessToken');
|
||||
});
|
||||
|
||||
it('should throw an error if the response is not ok', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://github.com/login/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Bad Request',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const code = 'invalidCode';
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(code)).toBeRejectedWithError(
|
||||
'Failed to exchange code for token: Bad Request'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if the response contains an error', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://github.com/login/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
error: 'invalid_grant',
|
||||
error_description: 'Code is invalid',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const code = 'invalidCode';
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(code)).toBeRejectedWithError('Code is invalid');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserFromAccessToken', function () {
|
||||
it('should fetch user data successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.github.com/user',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'mockUserId',
|
||||
login: 'mockUserLogin',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'validAccessToken';
|
||||
const user = await adapter.getUserFromAccessToken(accessToken);
|
||||
|
||||
expect(user).toEqual({ id: 'mockUserId', login: 'mockUserLogin' });
|
||||
});
|
||||
|
||||
it('should throw an error if the response is not ok', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.github.com/user',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'invalidAccessToken';
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken(accessToken)).toBeRejectedWithError(
|
||||
'Failed to fetch GitHub user: Unauthorized'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if user data is invalid', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.github.com/user',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'validAccessToken';
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken(accessToken)).toBeRejectedWithError(
|
||||
'Invalid GitHub user data received.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GitHubAdapter E2E Test', function () {
|
||||
beforeEach(async function () {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
github: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should log in user using GitHub adapter successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://github.com/login/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.github.com/user',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'mockUserId',
|
||||
login: 'mockUserLogin',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode' };
|
||||
const user = await Parse.User.logInWith('github', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle error when GitHub returns invalid code', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://github.com/login/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Invalid code',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'invalidCode' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('github', { authData })).toBeRejectedWithError(
|
||||
'Failed to exchange code for token: Invalid code'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when GitHub returns invalid user data', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://github.com/login/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.github.com/user',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('github', { authData })).toBeRejectedWithError(
|
||||
'Failed to fetch GitHub user: Unauthorized'
|
||||
);
|
||||
});
|
||||
|
||||
it('e2e secure does not support insecure payload', async function () {
|
||||
mockFetch();
|
||||
const authData = { id: 'mockUserId', access_token: 'mockAccessToken123' };
|
||||
await expectAsync(Parse.User.logInWith('github', { authData })).toBeRejectedWithError(
|
||||
'GitHub code is required.'
|
||||
);
|
||||
});
|
||||
|
||||
it('e2e insecure does support secure payload', async function () {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
github: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
enableInsecureAuth: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://github.com/login/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.github.com/user',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'mockUserId',
|
||||
login: 'mockUserLogin',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode' };
|
||||
const user = await Parse.User.logInWith('github', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,356 +0,0 @@
|
||||
const GooglePlayGamesServicesAdapter = require('../../../lib/Adapters/Auth/gpgames').default;
|
||||
|
||||
describe('GooglePlayGamesServicesAdapter', function () {
|
||||
let adapter;
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new GooglePlayGamesServicesAdapter.constructor();
|
||||
adapter.clientId = 'validClientId';
|
||||
adapter.clientSecret = 'validClientSecret';
|
||||
});
|
||||
|
||||
describe('getAccessTokenFromCode', function () {
|
||||
it('should fetch an access token successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://oauth2.googleapis.com/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const code = 'validCode';
|
||||
const authData = { redirectUri: 'http://example.com' };
|
||||
const token = await adapter.getAccessTokenFromCode(code, authData);
|
||||
|
||||
expect(token).toBe('mockAccessToken');
|
||||
});
|
||||
|
||||
it('should throw an error if the response is not ok', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://oauth2.googleapis.com/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Bad Request',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const code = 'invalidCode';
|
||||
const authData = { redirectUri: 'http://example.com' };
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(code, authData)).toBeRejectedWithError(
|
||||
'Failed to exchange code for token: Bad Request'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if the response contains an error', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://oauth2.googleapis.com/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
error: 'invalid_grant',
|
||||
error_description: 'Code is invalid',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const code = 'invalidCode';
|
||||
const authData = { redirectUri: 'http://example.com' };
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(code, authData)).toBeRejectedWithError(
|
||||
'Code is invalid'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserFromAccessToken', function () {
|
||||
it('should fetch user data successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
playerId: 'mockUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'validAccessToken';
|
||||
const authData = { id: 'mockUserId' };
|
||||
const user = await adapter.getUserFromAccessToken(accessToken, authData);
|
||||
|
||||
expect(user).toEqual({ id: 'mockUserId' });
|
||||
});
|
||||
|
||||
it('should throw an error if the response is not ok', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'invalidAccessToken';
|
||||
const authData = { id: 'mockUserId' };
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken(accessToken, authData)).toBeRejectedWithError(
|
||||
'Failed to fetch Google Play Games Services user: Unauthorized'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if user data is invalid', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'validAccessToken';
|
||||
const authData = { id: 'mockUserId' };
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken(accessToken, authData)).toBeRejectedWithError(
|
||||
'Invalid Google Play Games Services user data received.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if playerId does not match the provided user ID', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
playerId: 'anotherUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'validAccessToken';
|
||||
const authData = { id: 'mockUserId' };
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken(accessToken, authData)).toBeRejectedWithError(
|
||||
'Invalid Google Play Games Services user data received.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GooglePlayGamesServicesAdapter E2E Test', function () {
|
||||
beforeEach(async function () {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
gpgames: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should log in user successfully with valid code', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://oauth2.googleapis.com/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
playerId: 'mockUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
id: 'mockUserId',
|
||||
redirectUri: 'http://example.com',
|
||||
};
|
||||
|
||||
const user = await Parse.User.logInWith('gpgames', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://oauth2.googleapis.com/token',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when the token exchange fails', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://oauth2.googleapis.com/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Invalid code',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'invalidCode',
|
||||
redirectUri: 'http://example.com',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('gpgames', { authData })).toBeRejectedWithError(
|
||||
'Failed to exchange code for token: Invalid code'
|
||||
);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://oauth2.googleapis.com/token',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when user data fetch fails', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://oauth2.googleapis.com/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
id: 'mockUserId',
|
||||
redirectUri: 'http://example.com',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('gpgames', { authData })).toBeRejectedWithError(
|
||||
'Failed to fetch Google Play Games Services user: Unauthorized'
|
||||
);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://oauth2.googleapis.com/token',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when user data is invalid', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://oauth2.googleapis.com/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
playerId: 'anotherUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
id: 'mockUserId',
|
||||
redirectUri: 'http://example.com',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('gpgames', { authData })).toBeRejectedWithError(
|
||||
'Invalid Google Play Games Services user data received.'
|
||||
);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://oauth2.googleapis.com/token',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://www.googleapis.com/games/v1/players/mockUserId',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when no code or access token is provided', async function () {
|
||||
mockFetch();
|
||||
|
||||
const authData = {
|
||||
id: 'mockUserId',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('gpgames', { authData })).toBeRejectedWithError(
|
||||
'gpgames code is required.'
|
||||
);
|
||||
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -1,283 +0,0 @@
|
||||
const InstagramAdapter = require('../../../lib/Adapters/Auth/instagram').default;
|
||||
|
||||
describe('InstagramAdapter', function () {
|
||||
let adapter;
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new InstagramAdapter.constructor();
|
||||
adapter.clientId = 'validClientId';
|
||||
adapter.clientSecret = 'validClientSecret';
|
||||
adapter.redirectUri = 'https://example.com/callback';
|
||||
});
|
||||
|
||||
describe('getAccessTokenFromCode', function () {
|
||||
it('should fetch an access token successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.instagram.com/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode' };
|
||||
const token = await adapter.getAccessTokenFromCode(authData);
|
||||
|
||||
expect(token).toBe('mockAccessToken');
|
||||
});
|
||||
|
||||
it('should throw an error if the response contains an error', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.instagram.com/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
error: 'invalid_grant',
|
||||
error_description: 'Code is invalid',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'invalidCode' };
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
|
||||
'Code is invalid'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserFromAccessToken', function () {
|
||||
it('should fetch user data successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'mockUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'mockAccessToken';
|
||||
const authData = { id: 'mockUserId' };
|
||||
const user = await adapter.getUserFromAccessToken(accessToken, authData);
|
||||
|
||||
expect(user).toEqual({ id: 'mockUserId' });
|
||||
});
|
||||
|
||||
it('should throw an error if user ID does not match authData', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'differentUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'mockAccessToken';
|
||||
const authData = { id: 'mockUserId' };
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken(accessToken, authData)).toBeRejectedWithError(
|
||||
'Instagram auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should ignore client-provided apiURL and use hardcoded endpoint', async () => {
|
||||
const accessToken = 'mockAccessToken';
|
||||
const authData = {
|
||||
id: 'mockUserId',
|
||||
apiURL: 'https://example.com/',
|
||||
};
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'mockUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const user = await adapter.getUserFromAccessToken(accessToken, authData);
|
||||
expect(user).toEqual({ id: 'mockUserId' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('InstagramAdapter E2E Test', function () {
|
||||
beforeEach(async function () {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
instagram: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
redirectUri: 'https://example.com/callback',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should log in user successfully with valid code', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.instagram.com/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken123',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'mockUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
id: 'mockUserId',
|
||||
};
|
||||
|
||||
const user = await Parse.User.logInWith('instagram', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle error when access token exchange fails', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.instagram.com/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Invalid code',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'invalidCode',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('instagram', { authData })).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Instagram API request failed.')
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when user data fetch fails', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.instagram.com/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken123',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
id: 'mockUserId',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('instagram', { authData })).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Instagram API request failed.')
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when user data is invalid', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.instagram.com/oauth/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken123',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'differentUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
id: 'mockUserId',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('instagram', { authData })).toBeRejectedWithError(
|
||||
'Instagram auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when no code or access token is provided', async function () {
|
||||
mockFetch();
|
||||
|
||||
const authData = {
|
||||
id: 'mockUserId',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('instagram', { authData })).toBeRejectedWithError(
|
||||
'Instagram code is required.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,309 +0,0 @@
|
||||
const LineAdapter = require('../../../lib/Adapters/Auth/line').default;
|
||||
describe('LineAdapter', function () {
|
||||
let adapter;
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new LineAdapter.constructor();
|
||||
adapter.clientId = 'validClientId';
|
||||
adapter.clientSecret = 'validClientSecret';
|
||||
});
|
||||
|
||||
describe('getAccessTokenFromCode', function () {
|
||||
it('should throw an error if code is missing in authData', async function () {
|
||||
const authData = { redirect_uri: 'http://example.com' };
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
|
||||
'Line auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should fetch an access token successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/oauth2/v2.1/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
redirect_uri: 'http://example.com',
|
||||
};
|
||||
|
||||
const token = await adapter.getAccessTokenFromCode(authData);
|
||||
|
||||
expect(token).toBe('mockAccessToken');
|
||||
});
|
||||
|
||||
it('should throw an error if response is not ok', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/oauth2/v2.1/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Bad Request',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'invalidCode',
|
||||
redirect_uri: 'http://example.com',
|
||||
};
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
|
||||
'Failed to exchange code for token: Bad Request'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if response contains an error object', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/oauth2/v2.1/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
error: 'invalid_grant',
|
||||
error_description: 'Code is invalid',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'invalidCode',
|
||||
redirect_uri: 'http://example.com',
|
||||
};
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
|
||||
'Code is invalid'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserFromAccessToken', function () {
|
||||
it('should fetch user data successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/v2/profile',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
userId: 'mockUserId',
|
||||
displayName: 'mockDisplayName',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'validAccessToken';
|
||||
const user = await adapter.getUserFromAccessToken(accessToken);
|
||||
|
||||
expect(user).toEqual({
|
||||
userId: 'mockUserId',
|
||||
displayName: 'mockDisplayName',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if response is not ok', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/v2/profile',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'invalidAccessToken';
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken(accessToken)).toBeRejectedWithError(
|
||||
'Failed to fetch Line user: Unauthorized'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if user data is invalid', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/v2/profile',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const accessToken = 'validAccessToken';
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken(accessToken)).toBeRejectedWithError(
|
||||
'Invalid Line user data received.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('LineAdapter E2E Test', function () {
|
||||
beforeEach(async function () {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
line: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should log in user successfully with valid code', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/oauth2/v2.1/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.line.me/v2/profile',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
userId: 'mockUserId',
|
||||
displayName: 'mockDisplayName',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
redirect_uri: 'http://example.com',
|
||||
};
|
||||
|
||||
const user = await Parse.User.logInWith('line', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle error when token exchange fails', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/oauth2/v2.1/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Invalid code',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'invalidCode',
|
||||
redirect_uri: 'http://example.com',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('line', { authData })).toBeRejectedWithError(
|
||||
'Failed to exchange code for token: Invalid code'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when user data fetch fails', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/oauth2/v2.1/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.line.me/v2/profile',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
redirect_uri: 'http://example.com',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('line', { authData })).toBeRejectedWithError(
|
||||
'Failed to fetch Line user: Unauthorized'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when user data is invalid', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.line.me/oauth2/v2.1/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.line.me/v2/profile',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
redirect_uri: 'http://example.com',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('line', { authData })).toBeRejectedWithError(
|
||||
'Invalid Line user data received.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when no code is provided', async function () {
|
||||
mockFetch();
|
||||
|
||||
const authData = {
|
||||
redirect_uri: 'http://example.com',
|
||||
};
|
||||
|
||||
await expectAsync(Parse.User.logInWith('line', { authData })).toBeRejectedWithError(
|
||||
'Line code is required.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,333 +0,0 @@
|
||||
|
||||
const LinkedInAdapter = require('../../../lib/Adapters/Auth/linkedin').default;
|
||||
describe('LinkedInAdapter', function () {
|
||||
let adapter;
|
||||
const validOptions = {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
enableInsecureAuth: false,
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new LinkedInAdapter.constructor();
|
||||
});
|
||||
|
||||
describe('Test configuration errors', function () {
|
||||
it('should throw error for missing options', function () {
|
||||
const invalidOptions = [null, undefined, {}, { clientId: 'validClientId' }];
|
||||
|
||||
for (const options of invalidOptions) {
|
||||
expect(() => {
|
||||
adapter.validateOptions(options);
|
||||
}).toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
it('should validate options successfully with valid parameters', function () {
|
||||
expect(() => {
|
||||
adapter.validateOptions(validOptions);
|
||||
}).not.toThrow();
|
||||
expect(adapter.clientId).toBe(validOptions.clientId);
|
||||
expect(adapter.clientSecret).toBe(validOptions.clientSecret);
|
||||
expect(adapter.enableInsecureAuth).toBe(validOptions.enableInsecureAuth);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test beforeFind', function () {
|
||||
it('should throw error for invalid payload', async function () {
|
||||
adapter.enableInsecureAuth = true;
|
||||
|
||||
const payloads = [{}, { access_token: null }];
|
||||
|
||||
for (const payload of payloads) {
|
||||
await expectAsync(adapter.beforeFind(payload)).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'LinkedIn auth is invalid for this user.')
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should process secure payload and set auth data', async function () {
|
||||
spyOn(adapter, 'getAccessTokenFromCode').and.returnValue(
|
||||
Promise.resolve('validToken')
|
||||
);
|
||||
spyOn(adapter, 'getUserFromAccessToken').and.returnValue(
|
||||
Promise.resolve({ id: 'validUserId' })
|
||||
);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com', is_mobile_sdk: false };
|
||||
|
||||
await adapter.beforeFind(authData);
|
||||
|
||||
expect(authData.access_token).toBe('validToken');
|
||||
expect(authData.id).toBe('validUserId');
|
||||
});
|
||||
|
||||
it('should validate insecure auth and match user id', async function () {
|
||||
adapter.enableInsecureAuth = true;
|
||||
spyOn(adapter, 'getUserFromAccessToken').and.returnValue(
|
||||
Promise.resolve({ id: 'validUserId' })
|
||||
);
|
||||
|
||||
const authData = { access_token: 'validToken', id: 'validUserId', is_mobile_sdk: false };
|
||||
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeResolved();
|
||||
});
|
||||
|
||||
it('should throw error if insecure auth user id does not match', async function () {
|
||||
adapter.enableInsecureAuth = true;
|
||||
spyOn(adapter, 'getUserFromAccessToken').and.returnValue(
|
||||
Promise.resolve({ id: 'invalidUserId' })
|
||||
);
|
||||
|
||||
const authData = { access_token: 'validToken', id: 'validUserId', is_mobile_sdk: false };
|
||||
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeRejectedWith(
|
||||
new Error('LinkedIn auth is invalid for this user.')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test getUserFromAccessToken', function () {
|
||||
it('should fetch user successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.linkedin.com/v2/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ id: 'validUserId' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const user = await adapter.getUserFromAccessToken('validToken', false);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith('https://api.linkedin.com/v2/me', {
|
||||
headers: {
|
||||
Authorization: `Bearer validToken`,
|
||||
'x-li-format': 'json',
|
||||
'x-li-src': undefined,
|
||||
},
|
||||
method: 'GET',
|
||||
});
|
||||
expect(user).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
|
||||
it('should throw error for invalid response', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.linkedin.com/v2/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken('invalidToken', false)).toBeRejectedWith(
|
||||
new Error('LinkedIn API request failed.')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test getAccessTokenFromCode', function () {
|
||||
it('should fetch token successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://www.linkedin.com/oauth/v2/accessToken',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validToken' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const tokenResponse = await adapter.getAccessTokenFromCode('validCode', 'http://example.com');
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith('https://www.linkedin.com/oauth/v2/accessToken', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: jasmine.any(URLSearchParams),
|
||||
});
|
||||
expect(tokenResponse).toEqual('validToken');
|
||||
});
|
||||
|
||||
it('should throw error for invalid response', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://www.linkedin.com/oauth/v2/accessToken',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(
|
||||
adapter.getAccessTokenFromCode('invalidCode', 'http://example.com')
|
||||
).toBeRejectedWith(new Error('LinkedIn API request failed.'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test validate methods', function () {
|
||||
const authData = { id: 'validUserId', access_token: 'validToken' };
|
||||
|
||||
it('validateLogin should return user id', function () {
|
||||
const result = adapter.validateLogin(authData);
|
||||
expect(result).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
|
||||
it('validateSetUp should return user id', function () {
|
||||
const result = adapter.validateSetUp(authData);
|
||||
expect(result).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
|
||||
it('validateUpdate should return user id', function () {
|
||||
const result = adapter.validateUpdate(authData);
|
||||
expect(result).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
|
||||
it('afterFind should return user id', function () {
|
||||
const result = adapter.afterFind(authData);
|
||||
expect(result).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('LinkedInAdapter E2E Test', function () {
|
||||
beforeEach(async function () {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
linkedin: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should log in user using LinkedIn adapter successfully (secure)', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://www.linkedin.com/oauth/v2/accessToken',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.linkedin.com/v2/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'mockUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'https://example.com/callback' };
|
||||
const user = await Parse.User.logInWith('linkedin', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://www.linkedin.com/oauth/v2/accessToken',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://api.linkedin.com/v2/me',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when LinkedIn returns invalid user data', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://www.linkedin.com/oauth/v2/accessToken',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
access_token: 'mockAccessToken123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.linkedin.com/v2/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'https://example.com/callback' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('linkedin', { authData })).toBeRejectedWithError(
|
||||
'LinkedIn API request failed.'
|
||||
);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://www.linkedin.com/oauth/v2/accessToken',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://api.linkedin.com/v2/me',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('secure does not support insecure payload if not enabled', async function () {
|
||||
mockFetch();
|
||||
const authData = { id: 'mockUserId', access_token: 'mockAccessToken123' };
|
||||
await expectAsync(Parse.User.logInWith('linkedin', { authData })).toBeRejectedWithError(
|
||||
'LinkedIn code is required.'
|
||||
);
|
||||
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('insecure mode supports insecure payload if enabled', async function () {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
linkedin: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
enableInsecureAuth: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.linkedin.com/v2/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
id: 'mockUserId',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { id: 'mockUserId', access_token: 'mockAccessToken123' };
|
||||
const user = await Parse.User.logInWith('linkedin', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://api.linkedin.com/v2/me',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,307 +0,0 @@
|
||||
const MicrosoftAdapter = require('../../../lib/Adapters/Auth/microsoft').default;
|
||||
|
||||
describe('MicrosoftAdapter', function () {
|
||||
let adapter;
|
||||
const validOptions = {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
enableInsecureAuth: false,
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new MicrosoftAdapter.constructor();
|
||||
});
|
||||
|
||||
describe('Test configuration errors', function () {
|
||||
it('should throw error for missing options', function () {
|
||||
const invalidOptions = [null, undefined, {}, { clientId: 'validClientId' }];
|
||||
|
||||
for (const options of invalidOptions) {
|
||||
expect(() => {
|
||||
adapter.validateOptions(options);
|
||||
}).toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
it('should validate options successfully with valid parameters', function () {
|
||||
expect(() => {
|
||||
adapter.validateOptions(validOptions);
|
||||
}).not.toThrow();
|
||||
expect(adapter.clientId).toBe(validOptions.clientId);
|
||||
expect(adapter.clientSecret).toBe(validOptions.clientSecret);
|
||||
expect(adapter.enableInsecureAuth).toBe(validOptions.enableInsecureAuth);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test getUserFromAccessToken', function () {
|
||||
it('should fetch user successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.microsoft.com/v1.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ id: 'validUserId' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const user = await adapter.getUserFromAccessToken('validToken');
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith('https://graph.microsoft.com/v1.0/me', {
|
||||
headers: {
|
||||
Authorization: 'Bearer validToken',
|
||||
},
|
||||
method: 'GET',
|
||||
});
|
||||
expect(user).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
|
||||
it('should throw error for invalid response', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.microsoft.com/v1.0/me',
|
||||
method: 'GET',
|
||||
response: { ok: false },
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken('invalidToken')).toBeRejectedWith(
|
||||
new Error('Microsoft API request failed.')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test getAccessTokenFromCode', function () {
|
||||
it('should fetch token successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validToken' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com' };
|
||||
const token = await adapter.getAccessTokenFromCode(authData);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: jasmine.any(URLSearchParams),
|
||||
});
|
||||
expect(token).toEqual('validToken');
|
||||
});
|
||||
|
||||
it('should throw error for invalid response', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
method: 'POST',
|
||||
response: { ok: false },
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'invalidCode', redirect_uri: 'http://example.com' };
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWith(
|
||||
new Error('Microsoft API request failed.')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test secure authentication flow', function () {
|
||||
it('should exchange code for access token and fetch user data', async function () {
|
||||
spyOn(adapter, 'getAccessTokenFromCode').and.returnValue(Promise.resolve('validToken'));
|
||||
spyOn(adapter, 'getUserFromAccessToken').and.returnValue(Promise.resolve({ id: 'validUserId' }));
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com' };
|
||||
await adapter.beforeFind(authData);
|
||||
|
||||
expect(authData.access_token).toBe('validToken');
|
||||
expect(authData.id).toBe('validUserId');
|
||||
});
|
||||
|
||||
it('should throw error if user data cannot be fetched', async function () {
|
||||
spyOn(adapter, 'getAccessTokenFromCode').and.returnValue(Promise.resolve('validToken'));
|
||||
spyOn(adapter, 'getUserFromAccessToken').and.throwError('Microsoft API request failed.');
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com' };
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeRejectedWith(
|
||||
new Error('Microsoft API request failed.')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test insecure authentication flow', function () {
|
||||
beforeEach(function () {
|
||||
adapter.enableInsecureAuth = true;
|
||||
});
|
||||
|
||||
it('should validate insecure auth and match user id', async function () {
|
||||
spyOn(adapter, 'getUserFromAccessToken').and.returnValue(
|
||||
Promise.resolve({ id: 'validUserId' })
|
||||
);
|
||||
|
||||
const authData = { access_token: 'validToken', id: 'validUserId' };
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeResolved();
|
||||
});
|
||||
|
||||
it('should throw error if insecure auth user id does not match', async function () {
|
||||
spyOn(adapter, 'getUserFromAccessToken').and.returnValue(
|
||||
Promise.resolve({ id: 'invalidUserId' })
|
||||
);
|
||||
|
||||
const authData = { access_token: 'validToken', id: 'validUserId' };
|
||||
await expectAsync(adapter.beforeFind(authData)).toBeRejectedWith(
|
||||
new Error('Microsoft auth is invalid for this user.')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MicrosoftAdapter E2E Tests', () => {
|
||||
beforeEach(async () => {
|
||||
// Simulate reconfiguring the server with Microsoft auth options
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
microsoft: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
enableInsecureAuth: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should authenticate user successfully using MicrosoftAdapter', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validAccessToken' }),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://graph.microsoft.com/v1.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ id: 'user123' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
|
||||
const user = await Parse.User.logInWith('microsoft', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle invalid code error gracefully', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
method: 'POST',
|
||||
response: { ok: false, statusText: 'Invalid code' },
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'invalidCode', redirect_uri: 'http://example.com/callback' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('microsoft', { authData })).toBeRejectedWithError(
|
||||
'Microsoft API request failed.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when fetching user data fails', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validAccessToken' }),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://graph.microsoft.com/v1.0/me',
|
||||
method: 'GET',
|
||||
response: { ok: false, statusText: 'Unauthorized' },
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('microsoft', { authData })).toBeRejectedWithError(
|
||||
'Microsoft API request failed.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow insecure auth when enabled', async () => {
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.microsoft.com/v1.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
id: 'user123',
|
||||
}),
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
microsoft: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
enableInsecureAuth: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const authData = { access_token: 'validAccessToken', id: 'user123' };
|
||||
const user = await Parse.User.logInWith('microsoft', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should reject insecure auth when user id does not match', async () => {
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.microsoft.com/v1.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
id: 'incorrectUser',
|
||||
}),
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
microsoft: {
|
||||
clientId: 'validClientId',
|
||||
clientSecret: 'validClientSecret',
|
||||
enableInsecureAuth: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const authData = { access_token: 'validAccessToken', id: 'incorrectUserId' };
|
||||
await expectAsync(Parse.User.logInWith('microsoft', { authData })).toBeRejectedWithError(
|
||||
'Microsoft auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,448 +0,0 @@
|
||||
const OAuth2Adapter = require('../../../lib/Adapters/Auth/oauth2').default;
|
||||
|
||||
describe('OAuth2Adapter', () => {
|
||||
let adapter;
|
||||
|
||||
const validOptions = {
|
||||
tokenIntrospectionEndpointUrl: 'https://provider.com/introspect',
|
||||
useridField: 'sub',
|
||||
appidField: 'aud',
|
||||
appIds: ['valid-app-id'],
|
||||
authorizationHeader: 'Bearer validAuthToken',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
adapter = new OAuth2Adapter.constructor();
|
||||
adapter.validateOptions(validOptions);
|
||||
});
|
||||
|
||||
describe('validateAppId', () => {
|
||||
it('should validate app ID successfully', async () => {
|
||||
const authData = { access_token: 'validAccessToken' };
|
||||
const mockResponse = {
|
||||
[validOptions.appidField]: 'valid-app-id',
|
||||
};
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: validOptions.tokenIntrospectionEndpointUrl,
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(
|
||||
adapter.validateAppId(validOptions.appIds, authData, validOptions)
|
||||
).toBeResolved();
|
||||
});
|
||||
|
||||
it('should throw an error if app ID is invalid', async () => {
|
||||
const authData = { access_token: 'validAccessToken' };
|
||||
const mockResponse = {
|
||||
[validOptions.appidField]: 'invalid-app-id',
|
||||
};
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: validOptions.tokenIntrospectionEndpointUrl,
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(
|
||||
adapter.validateAppId(validOptions.appIds, authData, validOptions)
|
||||
).toBeRejectedWithError('OAuth2: Invalid app ID.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateAuthData', () => {
|
||||
it('should validate auth data successfully', async () => {
|
||||
const authData = { id: 'user-id', access_token: 'validAccessToken' };
|
||||
const mockResponse = {
|
||||
active: true,
|
||||
[validOptions.useridField]: 'user-id',
|
||||
};
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: validOptions.tokenIntrospectionEndpointUrl,
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(
|
||||
adapter.validateAuthData(authData, null, validOptions)
|
||||
).toBeResolvedTo({});
|
||||
});
|
||||
|
||||
it('should throw an error if the token is inactive', async () => {
|
||||
const authData = { id: 'user-id', access_token: 'validAccessToken' };
|
||||
const mockResponse = { active: false };
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: validOptions.tokenIntrospectionEndpointUrl,
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(
|
||||
adapter.validateAuthData(authData, null, validOptions)
|
||||
).toBeRejectedWith(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 access token is invalid for this user.'));
|
||||
});
|
||||
|
||||
it('should throw an error if user ID does not match', async () => {
|
||||
const authData = { id: 'user-id', access_token: 'validAccessToken' };
|
||||
const mockResponse = {
|
||||
active: true,
|
||||
[validOptions.useridField]: 'different-user-id',
|
||||
};
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: validOptions.tokenIntrospectionEndpointUrl,
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(
|
||||
adapter.validateAuthData(authData, null, validOptions)
|
||||
).toBeRejectedWithError('OAuth2 access token is invalid for this user.');
|
||||
});
|
||||
|
||||
it('should default useridField to sub and reject mismatched user ID', async () => {
|
||||
const adapterNoUseridField = new OAuth2Adapter.constructor();
|
||||
adapterNoUseridField.validateOptions({
|
||||
tokenIntrospectionEndpointUrl: 'https://provider.example.com/introspect',
|
||||
});
|
||||
|
||||
const authData = { id: 'victim-user-id', access_token: 'attackerToken' };
|
||||
const mockResponse = {
|
||||
active: true,
|
||||
sub: 'attacker-user-id',
|
||||
};
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://provider.example.com/introspect',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(
|
||||
adapterNoUseridField.validateAuthData(authData, null, {})
|
||||
).toBeRejectedWithError('OAuth2 access token is invalid for this user.');
|
||||
});
|
||||
|
||||
it('should default useridField to sub and accept matching user ID', async () => {
|
||||
const adapterNoUseridField = new OAuth2Adapter.constructor();
|
||||
adapterNoUseridField.validateOptions({
|
||||
tokenIntrospectionEndpointUrl: 'https://provider.example.com/introspect',
|
||||
});
|
||||
|
||||
const authData = { id: 'user-id', access_token: 'validAccessToken' };
|
||||
const mockResponse = {
|
||||
active: true,
|
||||
sub: 'user-id',
|
||||
};
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://provider.example.com/introspect',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(
|
||||
adapterNoUseridField.validateAuthData(authData, null, {})
|
||||
).toBeResolvedTo({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('requestTokenInfo', () => {
|
||||
it('should fetch token info successfully', async () => {
|
||||
const mockResponse = { active: true };
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: validOptions.tokenIntrospectionEndpointUrl,
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await adapter.requestTokenInfo(
|
||||
'validAccessToken',
|
||||
validOptions
|
||||
);
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('should throw an error if the introspection endpoint URL is missing', async () => {
|
||||
const options = { ...validOptions, tokenIntrospectionEndpointUrl: null };
|
||||
|
||||
expect(
|
||||
() => adapter.validateOptions(options)
|
||||
).toThrow(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 token introspection endpoint URL is missing.'));
|
||||
});
|
||||
|
||||
it('should throw an error if the response is not ok', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: validOptions.tokenIntrospectionEndpointUrl,
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Bad Request',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(
|
||||
adapter.requestTokenInfo('invalidAccessToken')
|
||||
).toBeRejectedWithError('OAuth2 token introspection request failed.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('OAuth2Adapter E2E Tests', () => {
|
||||
beforeEach(async () => {
|
||||
// Simulate reconfiguring the server with OAuth2 auth options
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
mockOauth: {
|
||||
tokenIntrospectionEndpointUrl: 'https://provider.com/introspect',
|
||||
useridField: 'sub',
|
||||
appidField: 'aud',
|
||||
appIds: ['valid-app-id'],
|
||||
authorizationHeader: 'Bearer validAuthToken',
|
||||
oauth2: true
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should validate and authenticate user successfully', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://provider.com/introspect',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
active: true,
|
||||
sub: 'user123',
|
||||
aud: 'valid-app-id',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { access_token: 'validAccessToken', id: 'user123' };
|
||||
const user = await Parse.User.logInWith('mockOauth', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
expect(user.get('authData').mockOauth.id).toEqual('user123');
|
||||
});
|
||||
|
||||
it('should reject authentication for inactive token', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://provider.com/introspect',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ active: false, aud: ['valid-app-id'] }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { access_token: 'inactiveToken', id: 'user123' };
|
||||
await expectAsync(Parse.User.logInWith('mockOauth', { authData })).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 access token is invalid for this user.')
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject authentication for mismatched user ID', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://provider.com/introspect',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
active: true,
|
||||
sub: 'different-user',
|
||||
aud: 'valid-app-id',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { access_token: 'validAccessToken', id: 'user123' };
|
||||
await expectAsync(Parse.User.logInWith('mockOauth', { authData })).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 access token is invalid for this user.')
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject authentication for invalid app ID', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://provider.com/introspect',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
active: true,
|
||||
sub: 'user123',
|
||||
aud: 'invalid-app-id',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { access_token: 'validAccessToken', id: 'user123' };
|
||||
await expectAsync(Parse.User.logInWith('mockOauth', { authData })).toBeRejectedWithError(
|
||||
'OAuth2: Invalid app ID.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should send the correct access token to the introspection endpoint during app ID validation', async () => {
|
||||
const capturedTokens = [];
|
||||
const originalFetch = global.fetch;
|
||||
try {
|
||||
global.fetch = async (url, options) => {
|
||||
if (typeof url === 'string' && url === 'https://provider.com/introspect') {
|
||||
const body = options?.body?.toString() || '';
|
||||
const token = new URLSearchParams(body).get('token');
|
||||
capturedTokens.push(token);
|
||||
return {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
active: true,
|
||||
sub: 'user123',
|
||||
aud: 'valid-app-id',
|
||||
}),
|
||||
};
|
||||
}
|
||||
return originalFetch(url, options);
|
||||
};
|
||||
|
||||
const authData = { access_token: 'myRealAccessToken', id: 'user123' };
|
||||
const user = await Parse.User.logInWith('mockOauth', { authData });
|
||||
expect(user.id).toBeDefined();
|
||||
|
||||
// With appidField configured, validateAppId and validateAuthData both call requestTokenInfo.
|
||||
// Both should receive the actual access token, not 'undefined' from argument mismatch.
|
||||
expect(capturedTokens.length).toBeGreaterThanOrEqual(2);
|
||||
for (const token of capturedTokens) {
|
||||
expect(token).toBe('myRealAccessToken');
|
||||
}
|
||||
} finally {
|
||||
global.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject account takeover when useridField is omitted and attacker uses their own token with victim ID', async () => {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
mockOauth: {
|
||||
tokenIntrospectionEndpointUrl: 'https://provider.example.com/introspect',
|
||||
authorizationHeader: 'Bearer validAuthToken',
|
||||
oauth2: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Victim signs up with their own valid token
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://provider.example.com/introspect',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
active: true,
|
||||
sub: 'victim-sub-id',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const victimAuthData = { access_token: 'victimToken', id: 'victim-sub-id' };
|
||||
const victim = await Parse.User.logInWith('mockOauth', { authData: victimAuthData });
|
||||
expect(victim.id).toBeDefined();
|
||||
|
||||
// Attacker tries to log in with their own valid token but claims victim's ID
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://provider.example.com/introspect',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
active: true,
|
||||
sub: 'attacker-sub-id',
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const attackerAuthData = { access_token: 'attackerToken', id: 'victim-sub-id' };
|
||||
await expectAsync(Parse.User.logInWith('mockOauth', { authData: attackerAuthData })).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 access token is invalid for this user.')
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when token introspection endpoint is missing', async () => {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
mockOauth: {
|
||||
tokenIntrospectionEndpointUrl: null,
|
||||
useridField: 'sub',
|
||||
appidField: 'aud',
|
||||
appIds: ['valid-app-id'],
|
||||
authorizationHeader: 'Bearer validAuthToken',
|
||||
oauth2: true
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const authData = { access_token: 'validAccessToken', id: 'user123' };
|
||||
await expectAsync(Parse.User.logInWith('mockOauth', { authData })).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'OAuth2 token introspection endpoint URL is missing.')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,252 +0,0 @@
|
||||
const QqAdapter = require('../../../lib/Adapters/Auth/qq').default;
|
||||
|
||||
describe('QqAdapter', () => {
|
||||
let adapter;
|
||||
|
||||
beforeEach(() => {
|
||||
adapter = new QqAdapter.constructor();
|
||||
});
|
||||
|
||||
describe('getUserFromAccessToken', () => {
|
||||
it('should fetch user data successfully', async () => {
|
||||
const mockResponse = `callback({"client_id":"validAppId","openid":"user123"})`;
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
text: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await adapter.getUserFromAccessToken('validAccessToken');
|
||||
|
||||
expect(result).toEqual({ client_id: 'validAppId', openid: 'user123' });
|
||||
});
|
||||
|
||||
it('should throw an error if the API request fails', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(
|
||||
adapter.getUserFromAccessToken('invalidAccessToken')
|
||||
).toBeRejectedWithError('qq API request failed.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAccessTokenFromCode', () => {
|
||||
it('should fetch access token successfully', async () => {
|
||||
const mockResponse = `callback({"access_token":"validAccessToken","expires_in":3600,"refresh_token":"refreshToken"})`;
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/token',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
text: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await adapter.getAccessTokenFromCode({
|
||||
code: 'validCode',
|
||||
redirect_uri: 'https://your-redirect-uri.com/callback',
|
||||
});
|
||||
|
||||
expect(result).toBe('validAccessToken');
|
||||
});
|
||||
|
||||
it('should throw an error if the API request fails', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/token',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Bad Request',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(
|
||||
adapter.getAccessTokenFromCode({
|
||||
code: 'invalidCode',
|
||||
redirect_uri: 'https://your-redirect-uri.com/callback',
|
||||
})
|
||||
).toBeRejectedWithError('qq API request failed.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseResponseData', () => {
|
||||
it('should parse valid callback response data', () => {
|
||||
const response = `callback({"key":"value"})`;
|
||||
const result = adapter.parseResponseData(response);
|
||||
|
||||
expect(result).toEqual({ key: 'value' });
|
||||
});
|
||||
|
||||
it('should throw an error if the response data is invalid', () => {
|
||||
const response = 'invalid response';
|
||||
|
||||
expect(() => adapter.parseResponseData(response)).toThrowError(
|
||||
'qq auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('QqAdapter E2E Test', () => {
|
||||
beforeEach(async () => {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
qq: {
|
||||
clientId: 'validAppId',
|
||||
clientSecret: 'validAppSecret',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should log in user using Qq adapter successfully', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/token',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
text: () =>
|
||||
Promise.resolve(
|
||||
`callback({"access_token":"mockAccessToken","expires_in":3600})`
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
text: () =>
|
||||
Promise.resolve(
|
||||
`callback({"client_id":"validAppId","openid":"user123"})`
|
||||
),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'https://your-redirect-uri.com/callback' };
|
||||
const user = await Parse.User.logInWith('qq', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle error when Qq returns invalid code', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/token',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Invalid code',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'invalidCode', redirect_uri: 'https://your-redirect-uri.com/callback' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('qq', { authData })).toBeRejectedWithError(
|
||||
'qq API request failed.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when Qq returns invalid user data', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/token',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
text: () =>
|
||||
Promise.resolve(
|
||||
`callback({"access_token":"mockAccessToken","expires_in":3600})`
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'https://your-redirect-uri.com/callback' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('qq', { authData })).toBeRejectedWithError(
|
||||
'qq API request failed.'
|
||||
);
|
||||
});
|
||||
|
||||
it('e2e secure does not support insecure payload', async () => {
|
||||
mockFetch();
|
||||
const authData = { id: 'mockUserId', access_token: 'mockAccessToken' };
|
||||
await expectAsync(Parse.User.logInWith('qq', { authData })).toBeRejectedWithError(
|
||||
'qq code is required.'
|
||||
);
|
||||
});
|
||||
|
||||
it('e2e insecure does support secure payload', async () => {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
qq: {
|
||||
appId: 'validAppId',
|
||||
appSecret: 'validAppSecret',
|
||||
enableInsecureAuth: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/token',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
text: () =>
|
||||
Promise.resolve(
|
||||
`callback({"access_token":"mockAccessToken","expires_in":3600})`
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://graph.qq.com/oauth2.0/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
text: () =>
|
||||
Promise.resolve(
|
||||
`callback({"client_id":"validAppId","openid":"user123"})`
|
||||
),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'https://your-redirect-uri.com/callback' };
|
||||
const user = await Parse.User.logInWith('qq', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,113 +0,0 @@
|
||||
const SpotifyAdapter = require('../../../lib/Adapters/Auth/spotify').default;
|
||||
|
||||
describe('SpotifyAdapter', () => {
|
||||
let adapter;
|
||||
|
||||
beforeEach(() => {
|
||||
adapter = new SpotifyAdapter.constructor();
|
||||
});
|
||||
|
||||
describe('getUserFromAccessToken', () => {
|
||||
it('should fetch user data successfully', async () => {
|
||||
const mockResponse = {
|
||||
id: 'spotifyUser123',
|
||||
};
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.spotify.com/v1/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await adapter.getUserFromAccessToken('validAccessToken');
|
||||
|
||||
expect(result).toEqual({ id: 'spotifyUser123' });
|
||||
});
|
||||
|
||||
it('should throw an error if the API request fails', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.spotify.com/v1/me',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken('invalidAccessToken')).toBeRejectedWithError(
|
||||
'Spotify API request failed.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAccessTokenFromCode', () => {
|
||||
it('should fetch access token successfully', async () => {
|
||||
const mockResponse = {
|
||||
access_token: 'validAccessToken',
|
||||
expires_in: 3600,
|
||||
refresh_token: 'refreshToken',
|
||||
};
|
||||
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://accounts.spotify.com/api/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'validCode',
|
||||
redirect_uri: 'https://your-redirect-uri.com/callback',
|
||||
code_verifier: 'validCodeVerifier',
|
||||
};
|
||||
|
||||
const result = await adapter.getAccessTokenFromCode(authData);
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('should throw an error if authData is missing required fields', async () => {
|
||||
const authData = {
|
||||
redirect_uri: 'https://your-redirect-uri.com/callback',
|
||||
};
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
|
||||
'Spotify auth configuration authData.code and/or authData.redirect_uri and/or authData.code_verifier.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if the API request fails', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://accounts.spotify.com/api/token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
statusText: 'Bad Request',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = {
|
||||
code: 'invalidCode',
|
||||
redirect_uri: 'https://your-redirect-uri.com/callback',
|
||||
code_verifier: 'invalidCodeVerifier',
|
||||
};
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWithError(
|
||||
'Spotify API request failed.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,120 +0,0 @@
|
||||
const TwitterAuthAdapter = require('../../../lib/Adapters/Auth/twitter').default;
|
||||
|
||||
describe('TwitterAuthAdapter', function () {
|
||||
let adapter;
|
||||
const validOptions = {
|
||||
consumer_key: 'validConsumerKey',
|
||||
consumer_secret: 'validConsumerSecret',
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new TwitterAuthAdapter.constructor();
|
||||
});
|
||||
|
||||
describe('Test configuration errors', function () {
|
||||
it('should throw an error when options are missing', function () {
|
||||
expect(() => adapter.validateOptions()).toThrowError('Twitter auth options are required.');
|
||||
});
|
||||
|
||||
it('should throw an error when consumer_key and consumer_secret are missing for secure auth', function () {
|
||||
const options = { enableInsecureAuth: false };
|
||||
expect(() => adapter.validateOptions(options)).toThrowError(
|
||||
'Consumer key and secret are required for secure Twitter auth.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should not throw an error when valid options are provided', function () {
|
||||
expect(() => adapter.validateOptions(validOptions)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Validate Insecure Auth', function () {
|
||||
it('should throw an error if oauth_token or oauth_token_secret are missing', async function () {
|
||||
const authData = { oauth_token: 'validToken' }; // Missing oauth_token_secret
|
||||
await expectAsync(adapter.validateInsecureAuth(authData, validOptions)).toBeRejectedWithError(
|
||||
'Twitter insecure auth requires oauth_token and oauth_token_secret.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should validate insecure auth successfully when data matches', async function () {
|
||||
spyOn(adapter, 'request').and.returnValue(
|
||||
Promise.resolve({
|
||||
json: () => Promise.resolve({ id: 'validUserId' }),
|
||||
})
|
||||
);
|
||||
|
||||
const authData = {
|
||||
id: 'validUserId',
|
||||
oauth_token: 'validToken',
|
||||
oauth_token_secret: 'validSecret',
|
||||
};
|
||||
await expectAsync(adapter.validateInsecureAuth(authData, validOptions)).toBeResolved();
|
||||
});
|
||||
|
||||
it('should throw an error when user ID does not match', async function () {
|
||||
spyOn(adapter, 'request').and.returnValue(
|
||||
Promise.resolve({
|
||||
json: () => Promise.resolve({ id: 'invalidUserId' }),
|
||||
})
|
||||
);
|
||||
|
||||
const authData = {
|
||||
id: 'validUserId',
|
||||
oauth_token: 'validToken',
|
||||
oauth_token_secret: 'validSecret',
|
||||
};
|
||||
await expectAsync(adapter.validateInsecureAuth(authData, validOptions)).toBeRejectedWithError(
|
||||
'Twitter auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('End-to-End Tests', function () {
|
||||
beforeEach(async function () {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
twitter: validOptions,
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
it('should authenticate user successfully using validateAuthData', async function () {
|
||||
spyOn(adapter, 'exchangeAccessToken').and.returnValue(
|
||||
Promise.resolve({ oauth_token: 'validToken', user_id: 'validUserId' })
|
||||
);
|
||||
|
||||
const authData = {
|
||||
oauth_token: 'validToken',
|
||||
oauth_verifier: 'validVerifier',
|
||||
};
|
||||
await expectAsync(adapter.validateAuthData(authData, validOptions)).toBeResolved();
|
||||
expect(authData.id).toBe('validUserId');
|
||||
expect(authData.auth_token).toBe('validToken');
|
||||
});
|
||||
|
||||
it('should handle multiple configurations and validate successfully', async function () {
|
||||
const authData = {
|
||||
consumer_key: 'validConsumerKey',
|
||||
oauth_token: 'validToken',
|
||||
oauth_token_secret: 'validSecret',
|
||||
};
|
||||
|
||||
const optionsArray = [
|
||||
{ consumer_key: 'invalidKey', consumer_secret: 'invalidSecret' },
|
||||
validOptions,
|
||||
];
|
||||
|
||||
const selectedOption = adapter.handleMultipleConfigurations(authData, optionsArray);
|
||||
expect(selectedOption).toEqual(validOptions);
|
||||
});
|
||||
|
||||
it('should throw an error when no matching configuration is found', function () {
|
||||
const authData = { consumer_key: 'missingKey' };
|
||||
const optionsArray = [validOptions];
|
||||
|
||||
expect(() => adapter.handleMultipleConfigurations(authData, optionsArray)).toThrowError(
|
||||
'Twitter auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,236 +0,0 @@
|
||||
const WeChatAdapter = require('../../../lib/Adapters/Auth/wechat').default;
|
||||
|
||||
describe('WeChatAdapter', function () {
|
||||
let adapter;
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new WeChatAdapter.constructor();
|
||||
});
|
||||
|
||||
describe('Test getUserFromAccessToken', function () {
|
||||
it('should fetch user successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/auth?access_token=validToken&openid=validOpenId',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ errcode: 0, id: 'validUserId' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const user = await adapter.getUserFromAccessToken('validToken', { id: 'validOpenId' });
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://api.weixin.qq.com/sns/auth?access_token=validToken&openid=validOpenId',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
expect(user).toEqual({ errcode: 0, id: 'validUserId' });
|
||||
});
|
||||
|
||||
it('should throw error for invalid response', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/auth?access_token=invalidToken&openid=undefined',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
json: () => Promise.resolve({ errcode: 40013, errmsg: 'Invalid token' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await expectAsync(adapter.getUserFromAccessToken('invalidToken', 'invalidOpenId')).toBeRejectedWith(
|
||||
jasmine.objectContaining({ message: 'WeChat auth is invalid for this user.' })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test getAccessTokenFromCode', function () {
|
||||
it('should fetch access token successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=validCode&grant_type=authorization_code',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validToken', errcode: 0 }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
adapter.validateOptions({ clientId: 'validAppId', clientSecret: 'validAppSecret' });
|
||||
const authData = { code: 'validCode' };
|
||||
const token = await adapter.getAccessTokenFromCode(authData);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=validCode&grant_type=authorization_code',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
expect(token).toEqual('validToken');
|
||||
});
|
||||
|
||||
it('should throw error for invalid response', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=invalidCode&grant_type=authorization_code',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
json: () => Promise.resolve({ errcode: 40029, errmsg: 'Invalid code' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
adapter.validateOptions({ clientId: 'validAppId', clientSecret: 'validAppSecret' });
|
||||
|
||||
const authData = { code: 'invalidCode' };
|
||||
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWith(
|
||||
jasmine.objectContaining({ message: 'WeChat auth is invalid for this user.' })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('WeChatAdapter E2E Tests', function () {
|
||||
beforeEach(async () => {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
wechat: {
|
||||
clientId: 'validAppId',
|
||||
clientSecret: 'validAppSecret',
|
||||
enableInsecureAuth: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should authenticate user successfully using WeChatAdapter', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=validCode&grant_type=authorization_code',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validAccessToken', openid: 'user123', errcode: 0 }),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/auth?access_token=validAccessToken&openid=user123',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ errcode: 0, id: 'user123' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
|
||||
const user = await Parse.User.logInWith('wechat', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle invalid code error gracefully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=invalidCode&grant_type=authorization_code',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
json: () => Promise.resolve({ errcode: 40029, errmsg: 'Invalid code' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'invalidCode', redirect_uri: 'http://example.com/callback' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('wechat', { authData })).toBeRejectedWith(
|
||||
jasmine.objectContaining({ message: 'WeChat auth is invalid for this user.' })
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when fetching user data fails', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=validCode&grant_type=authorization_code',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validAccessToken', openid: 'user123', errcode: 0 }),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/auth?access_token=validAccessToken&openid=user123',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: false,
|
||||
json: () => Promise.resolve({ errcode: 40013, errmsg: 'Invalid token' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
|
||||
|
||||
await expectAsync(Parse.User.logInWith('wechat', { authData })).toBeRejectedWith(
|
||||
jasmine.objectContaining({ message: 'WeChat auth is invalid for this user.' })
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow insecure auth when enabled', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/auth?access_token=validAccessToken&openid=user123',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ errcode: 0, id: 'user123' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
wechat: {
|
||||
appId: 'validAppId',
|
||||
appSecret: 'validAppSecret',
|
||||
enableInsecureAuth: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const authData = { access_token: 'validAccessToken', id: 'user123' };
|
||||
const user = await Parse.User.logInWith('wechat', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should reject insecure auth when user id does not match', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weixin.qq.com/sns/auth?access_token=validAccessToken&openid=incorrectUserId',
|
||||
method: 'GET',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ errcode: 0, id: 'incorrectUser' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
wechat: {
|
||||
appId: 'validAppId',
|
||||
appSecret: 'validAppSecret',
|
||||
enableInsecureAuth: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const authData = { access_token: 'validAccessToken', id: 'incorrectUserId' };
|
||||
await expectAsync(Parse.User.logInWith('wechat', { authData })).toBeRejectedWith(
|
||||
jasmine.objectContaining({ message: 'WeChat auth is invalid for this user.' })
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,204 +0,0 @@
|
||||
const WeiboAdapter = require('../../../lib/Adapters/Auth/weibo').default;
|
||||
|
||||
describe('WeiboAdapter', function () {
|
||||
let adapter;
|
||||
|
||||
beforeEach(function () {
|
||||
adapter = new WeiboAdapter.constructor();
|
||||
});
|
||||
|
||||
describe('Test configuration errors', function () {
|
||||
it('should throw error if code or redirect_uri is missing', async function () {
|
||||
const invalidAuthData = [
|
||||
{},
|
||||
{ code: 'validCode' },
|
||||
{ redirect_uri: 'http://example.com/callback' },
|
||||
];
|
||||
|
||||
for (const authData of invalidAuthData) {
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWith(
|
||||
jasmine.objectContaining({
|
||||
message: 'Weibo auth requires code and redirect_uri to be sent.',
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test getUserFromAccessToken', function () {
|
||||
it('should fetch user successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weibo.com/oauth2/get_token_info',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ uid: 'validUserId' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { id: 'validUserId' };
|
||||
const user = await adapter.getUserFromAccessToken('validToken', authData);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://api.weibo.com/oauth2/get_token_info',
|
||||
jasmine.objectContaining({
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
})
|
||||
);
|
||||
expect(user).toEqual({ id: 'validUserId' });
|
||||
});
|
||||
|
||||
it('should throw error for invalid response', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weibo.com/oauth2/get_token_info',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
json: () => Promise.resolve({}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { id: 'invalidUserId' };
|
||||
await expectAsync(adapter.getUserFromAccessToken('invalidToken', authData)).toBeRejectedWith(
|
||||
jasmine.objectContaining({
|
||||
message: 'Weibo auth is invalid for this user.',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test getAccessTokenFromCode', function () {
|
||||
it('should fetch access token successfully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weibo.com/oauth2/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validToken', uid: 'validUserId' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
|
||||
const token = await adapter.getAccessTokenFromCode(authData);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://api.weibo.com/oauth2/access_token',
|
||||
jasmine.objectContaining({
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
})
|
||||
);
|
||||
expect(token).toEqual('validToken');
|
||||
});
|
||||
|
||||
it('should throw error for invalid response', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weibo.com/oauth2/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
json: () => Promise.resolve({ errcode: 40029 }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'invalidCode', redirect_uri: 'http://example.com/callback' };
|
||||
await expectAsync(adapter.getAccessTokenFromCode(authData)).toBeRejectedWith(
|
||||
jasmine.objectContaining({
|
||||
message: 'Weibo auth is invalid for this user.',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('WeiboAdapter E2E Tests', function () {
|
||||
beforeEach(async () => {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
weibo: {
|
||||
clientId: 'validAppId',
|
||||
clientSecret: 'validAppSecret',
|
||||
},
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should authenticate user successfully using WeiboAdapter', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weibo.com/oauth2/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validAccessToken', uid: 'user123' }),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.weibo.com/oauth2/get_token_info',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ uid: 'user123' }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
|
||||
const user = await Parse.User.logInWith('weibo', { authData });
|
||||
|
||||
expect(user.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle invalid code error gracefully', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weibo.com/oauth2/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
json: () => Promise.resolve({ errcode: 40029 }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'invalidCode', redirect_uri: 'http://example.com/callback' };
|
||||
await expectAsync(Parse.User.logInWith('weibo', { authData })).toBeRejectedWith(
|
||||
jasmine.objectContaining({ message: 'Weibo auth is invalid for this user.' })
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when fetching user data fails', async function () {
|
||||
mockFetch([
|
||||
{
|
||||
url: 'https://api.weibo.com/oauth2/access_token',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ access_token: 'validAccessToken', uid: 'user123' }),
|
||||
},
|
||||
},
|
||||
{
|
||||
url: 'https://api.weibo.com/oauth2/get_token_info',
|
||||
method: 'POST',
|
||||
response: {
|
||||
ok: false,
|
||||
json: () => Promise.resolve({}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const authData = { code: 'validCode', redirect_uri: 'http://example.com/callback' };
|
||||
await expectAsync(Parse.User.logInWith('weibo', { authData })).toBeRejectedWith(
|
||||
jasmine.objectContaining({ message: 'Weibo auth is invalid for this user.' })
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
+19
-41
@@ -5,13 +5,6 @@ const request = require('../lib/request');
|
||||
const AudiencesRouter = require('../lib/Routers/AudiencesRouter').AudiencesRouter;
|
||||
|
||||
describe('AudiencesRouter', () => {
|
||||
let loggerErrorSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
const logger = require('../lib/logger').default;
|
||||
loggerErrorSpy = spyOn(logger, 'error').and.callThrough();
|
||||
});
|
||||
|
||||
it('uses find condition from request.body', done => {
|
||||
const config = Config.get('test');
|
||||
const androidAudienceRequest = {
|
||||
@@ -36,9 +29,9 @@ describe('AudiencesRouter', () => {
|
||||
|
||||
const router = new AudiencesRouter();
|
||||
rest
|
||||
.create(config, auth.master(config), '_Audience', androidAudienceRequest)
|
||||
.create(config, auth.nobody(config), '_Audience', androidAudienceRequest)
|
||||
.then(() => {
|
||||
return rest.create(config, auth.master(config), '_Audience', iosAudienceRequest);
|
||||
return rest.create(config, auth.nobody(config), '_Audience', iosAudienceRequest);
|
||||
})
|
||||
.then(() => {
|
||||
return router.handleFind(request);
|
||||
@@ -78,9 +71,9 @@ describe('AudiencesRouter', () => {
|
||||
|
||||
const router = new AudiencesRouter();
|
||||
rest
|
||||
.create(config, auth.master(config), '_Audience', androidAudienceRequest)
|
||||
.create(config, auth.nobody(config), '_Audience', androidAudienceRequest)
|
||||
.then(() => {
|
||||
return rest.create(config, auth.master(config), '_Audience', iosAudienceRequest);
|
||||
return rest.create(config, auth.nobody(config), '_Audience', iosAudienceRequest);
|
||||
})
|
||||
.then(() => {
|
||||
return router.handleFind(request);
|
||||
@@ -119,9 +112,9 @@ describe('AudiencesRouter', () => {
|
||||
Config.get('test');
|
||||
const router = new AudiencesRouter();
|
||||
rest
|
||||
.create(config, auth.master(config), '_Audience', androidAudienceRequest)
|
||||
.create(config, auth.nobody(config), '_Audience', androidAudienceRequest)
|
||||
.then(() => {
|
||||
return rest.create(config, auth.master(config), '_Audience', iosAudienceRequest);
|
||||
return rest.create(config, auth.nobody(config), '_Audience', iosAudienceRequest);
|
||||
})
|
||||
.then(() => {
|
||||
return router.handleFind(request);
|
||||
@@ -159,8 +152,8 @@ describe('AudiencesRouter', () => {
|
||||
|
||||
const router = new AudiencesRouter();
|
||||
rest
|
||||
.create(config, auth.master(config), '_Audience', androidAudienceRequest)
|
||||
.then(() => rest.create(config, auth.master(config), '_Audience', iosAudienceRequest))
|
||||
.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;
|
||||
@@ -197,9 +190,9 @@ describe('AudiencesRouter', () => {
|
||||
|
||||
const router = new AudiencesRouter();
|
||||
rest
|
||||
.create(config, auth.master(config), '_Audience', androidAudienceRequest)
|
||||
.create(config, auth.nobody(config), '_Audience', androidAudienceRequest)
|
||||
.then(() => {
|
||||
return rest.create(config, auth.master(config), '_Audience', iosAudienceRequest);
|
||||
return rest.create(config, auth.nobody(config), '_Audience', iosAudienceRequest);
|
||||
})
|
||||
.then(() => {
|
||||
return router.handleFind(request);
|
||||
@@ -270,71 +263,61 @@ describe('AudiencesRouter', () => {
|
||||
});
|
||||
|
||||
it('should only create with master key', done => {
|
||||
loggerErrorSpy.calls.reset();
|
||||
Parse._request('POST', 'push_audiences', {
|
||||
name: 'My Audience',
|
||||
query: JSON.stringify({ deviceType: 'ios' }),
|
||||
}).then(
|
||||
() => {},
|
||||
error => {
|
||||
expect(error.message).toEqual('Permission denied');
|
||||
expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required'));
|
||||
expect(error.message).toEqual('unauthorized: master key is required');
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should only find with master key', done => {
|
||||
loggerErrorSpy.calls.reset();
|
||||
Parse._request('GET', 'push_audiences', {}).then(
|
||||
() => {},
|
||||
error => {
|
||||
expect(error.message).toEqual('Permission denied');
|
||||
expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required'));
|
||||
expect(error.message).toEqual('unauthorized: master key is required');
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should only get with master key', done => {
|
||||
loggerErrorSpy.calls.reset();
|
||||
Parse._request('GET', `push_audiences/someId`, {}).then(
|
||||
() => {},
|
||||
error => {
|
||||
expect(error.message).toEqual('Permission denied');
|
||||
expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required'));
|
||||
expect(error.message).toEqual('unauthorized: master key is required');
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should only update with master key', done => {
|
||||
loggerErrorSpy.calls.reset();
|
||||
Parse._request('PUT', `push_audiences/someId`, {
|
||||
name: 'My Audience 2',
|
||||
}).then(
|
||||
() => {},
|
||||
error => {
|
||||
expect(error.message).toEqual('Permission denied');
|
||||
expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required'));
|
||||
expect(error.message).toEqual('unauthorized: master key is required');
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should only delete with master key', done => {
|
||||
loggerErrorSpy.calls.reset();
|
||||
Parse._request('DELETE', `push_audiences/someId`, {}).then(
|
||||
() => {},
|
||||
error => {
|
||||
expect(error.message).toEqual('Permission denied');
|
||||
expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required'));
|
||||
expect(error.message).toEqual('unauthorized: master key is required');
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it_id('af1111b5-3251-4b40-8f06-fb0fc624fa91')(it_exclude_dbs(['postgres']))('should support legacy parse.com audience fields', done => {
|
||||
it_exclude_dbs(['postgres'])('should support legacy parse.com audience fields', done => {
|
||||
const database = Config.get(Parse.applicationId).database.adapter.database;
|
||||
const now = new Date();
|
||||
Parse._request(
|
||||
@@ -356,12 +339,11 @@ describe('AudiencesRouter', () => {
|
||||
)
|
||||
.then(result => {
|
||||
expect(result).toBeTruthy();
|
||||
|
||||
database
|
||||
.collection('test__Audience')
|
||||
.find({ _id: audience.objectId })
|
||||
.toArray()
|
||||
.then(rows => {
|
||||
.toArray((error, rows) => {
|
||||
expect(error).toEqual(undefined);
|
||||
expect(rows[0]['times_used']).toEqual(1);
|
||||
expect(rows[0]['_last_used']).toEqual(now);
|
||||
Parse._request(
|
||||
@@ -380,9 +362,6 @@ describe('AudiencesRouter', () => {
|
||||
.catch(error => {
|
||||
done.fail(error);
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
done.fail(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -421,7 +400,6 @@ describe('AudiencesRouter', () => {
|
||||
await reconfigureServer({
|
||||
appId: 'test',
|
||||
restAPIKey: 'test',
|
||||
masterKey: 'test',
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
});
|
||||
try {
|
||||
@@ -431,7 +409,7 @@ describe('AudiencesRouter', () => {
|
||||
body: { lorem: 'ipsum', _method: 'POST' },
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
'X-Parse-REST-API-Key': 'test',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
+40
-23
@@ -81,6 +81,17 @@ describe('Auth', () => {
|
||||
.then(roles => expect(roles).toEqual([]))
|
||||
.then(() => done());
|
||||
});
|
||||
|
||||
it('should properly handle bcrypt upgrade', done => {
|
||||
const bcryptOriginal = require('bcrypt-nodejs');
|
||||
const bcryptNew = require('bcryptjs');
|
||||
bcryptOriginal.hash('my1Long:password', null, null, function (err, res) {
|
||||
bcryptNew.compare('my1Long:password', res, function (err, res) {
|
||||
expect(res).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can use extendSessionOnUse', async () => {
|
||||
@@ -106,8 +117,6 @@ describe('Auth', () => {
|
||||
updatedAt: updatedAt.toISOString(),
|
||||
}
|
||||
);
|
||||
Parse.Server.cacheController.clear();
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await session.fetch();
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await session.fetch();
|
||||
@@ -143,6 +152,35 @@ describe('Auth', () => {
|
||||
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 = 100;
|
||||
|
||||
@@ -233,24 +271,3 @@ describe('Auth', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('extendSessionOnUse', () => {
|
||||
it(`shouldUpdateSessionExpiry()`, async () => {
|
||||
const { shouldUpdateSessionExpiry } = require('../lib/Auth');
|
||||
let update = new Date(Date.now() - 86410 * 1000);
|
||||
|
||||
const res = shouldUpdateSessionExpiry(
|
||||
{ sessionLength: 86460 },
|
||||
{ updatedAt: update }
|
||||
);
|
||||
|
||||
update = new Date(Date.now() - 43210 * 1000);
|
||||
const res2 = shouldUpdateSessionExpiry(
|
||||
{ sessionLength: 86460 },
|
||||
{ updatedAt: update }
|
||||
);
|
||||
|
||||
expect(res).toBe(true);
|
||||
expect(res2).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,210 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const request = require('../lib/request');
|
||||
const Config = require('../lib/Config');
|
||||
|
||||
describe('AuthData Unique Index', () => {
|
||||
const fakeAuthProvider = {
|
||||
validateAppId: () => Promise.resolve(),
|
||||
validateAuthData: () => Promise.resolve(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await reconfigureServer({ auth: { fakeAuthProvider } });
|
||||
});
|
||||
|
||||
it('should prevent concurrent signups with the same authData from creating duplicate users', async () => {
|
||||
const authData = { fakeAuthProvider: { id: 'duplicate-test-id', token: 'token1' } };
|
||||
|
||||
// Fire multiple concurrent signup requests with the same authData
|
||||
const concurrentRequests = Array.from({ length: 5 }, () =>
|
||||
request({
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
url: 'http://localhost:8378/1/users',
|
||||
body: { authData },
|
||||
}).then(
|
||||
response => ({ success: true, data: response.data }),
|
||||
error => ({ success: false, error: error.data || error.message })
|
||||
)
|
||||
);
|
||||
|
||||
const results = await Promise.all(concurrentRequests);
|
||||
const successes = results.filter(r => r.success);
|
||||
const failures = results.filter(r => !r.success);
|
||||
|
||||
// All should either succeed (returning the same user) or fail with "this auth is already used"
|
||||
// The key invariant: only ONE unique objectId should exist
|
||||
const uniqueObjectIds = new Set(successes.map(r => r.data.objectId));
|
||||
expect(uniqueObjectIds.size).toBe(1);
|
||||
|
||||
// Failures should be "this auth is already used" errors
|
||||
for (const failure of failures) {
|
||||
expect(failure.error.code).toBe(208);
|
||||
expect(failure.error.error).toBe('this auth is already used');
|
||||
}
|
||||
|
||||
// Verify only one user exists in the database with this authData
|
||||
const query = new Parse.Query('_User');
|
||||
query.equalTo('authData.fakeAuthProvider.id', 'duplicate-test-id');
|
||||
const users = await query.find({ useMasterKey: true });
|
||||
expect(users.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should prevent concurrent signups via batch endpoint with same authData', async () => {
|
||||
const authData = { fakeAuthProvider: { id: 'batch-race-test-id', token: 'token1' } };
|
||||
|
||||
const response = await request({
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
url: 'http://localhost:8378/1/batch',
|
||||
body: {
|
||||
requests: Array.from({ length: 3 }, () => ({
|
||||
method: 'POST',
|
||||
path: '/1/users',
|
||||
body: { authData },
|
||||
})),
|
||||
},
|
||||
});
|
||||
|
||||
const results = response.data;
|
||||
const successes = results.filter(r => r.success);
|
||||
const failures = results.filter(r => r.error);
|
||||
|
||||
// All successes should reference the same user
|
||||
const uniqueObjectIds = new Set(successes.map(r => r.success.objectId));
|
||||
expect(uniqueObjectIds.size).toBe(1);
|
||||
|
||||
// Failures should be "this auth is already used" errors
|
||||
for (const failure of failures) {
|
||||
expect(failure.error.code).toBe(208);
|
||||
expect(failure.error.error).toBe('this auth is already used');
|
||||
}
|
||||
|
||||
// Verify only one user exists in the database with this authData
|
||||
const query = new Parse.Query('_User');
|
||||
query.equalTo('authData.fakeAuthProvider.id', 'batch-race-test-id');
|
||||
const users = await query.find({ useMasterKey: true });
|
||||
expect(users.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should allow sequential signups with different authData IDs', async () => {
|
||||
const user1 = await Parse.User.logInWith('fakeAuthProvider', {
|
||||
authData: { id: 'user-id-1', token: 'token1' },
|
||||
});
|
||||
const user2 = await Parse.User.logInWith('fakeAuthProvider', {
|
||||
authData: { id: 'user-id-2', token: 'token2' },
|
||||
});
|
||||
|
||||
expect(user1.id).toBeDefined();
|
||||
expect(user2.id).toBeDefined();
|
||||
expect(user1.id).not.toBe(user2.id);
|
||||
});
|
||||
|
||||
it('should still allow login with authData after successful signup', async () => {
|
||||
const authPayload = { authData: { id: 'login-test-id', token: 'token1' } };
|
||||
|
||||
// Signup
|
||||
const user1 = await Parse.User.logInWith('fakeAuthProvider', authPayload);
|
||||
expect(user1.id).toBeDefined();
|
||||
|
||||
// Login again with same authData — should return same user
|
||||
const user2 = await Parse.User.logInWith('fakeAuthProvider', authPayload);
|
||||
expect(user2.id).toBe(user1.id);
|
||||
});
|
||||
|
||||
it('should skip startup index creation when createIndexAuthDataUniqueness is false', async () => {
|
||||
const config = Config.get('test');
|
||||
const adapter = config.database.adapter;
|
||||
const spy = spyOn(adapter, 'ensureAuthDataUniqueness').and.callThrough();
|
||||
|
||||
// Temporarily set the option to false
|
||||
const originalOptions = config.database.options.databaseOptions;
|
||||
config.database.options.databaseOptions = { createIndexAuthDataUniqueness: false };
|
||||
|
||||
await config.database.performInitialization();
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
|
||||
// Restore original options
|
||||
config.database.options.databaseOptions = originalOptions;
|
||||
});
|
||||
|
||||
it('should handle calling ensureAuthDataUniqueness multiple times (idempotent)', async () => {
|
||||
const config = Config.get('test');
|
||||
const adapter = config.database.adapter;
|
||||
|
||||
// Both calls should succeed (index creation is idempotent)
|
||||
await adapter.ensureAuthDataUniqueness('fakeAuthProvider');
|
||||
await adapter.ensureAuthDataUniqueness('fakeAuthProvider');
|
||||
});
|
||||
|
||||
it('should log warning when index creation fails due to existing duplicates', async () => {
|
||||
const config = Config.get('test');
|
||||
const adapter = config.database.adapter;
|
||||
|
||||
// Spy on the adapter to simulate a duplicate value error
|
||||
spyOn(adapter, 'ensureAuthDataUniqueness').and.callFake(() => {
|
||||
return Promise.reject(
|
||||
new Parse.Error(Parse.Error.DUPLICATE_VALUE, 'duplicates exist')
|
||||
);
|
||||
});
|
||||
|
||||
const logSpy = spyOn(require('../lib/logger').logger, 'warn');
|
||||
|
||||
// Re-run performInitialization — should warn but not throw
|
||||
await config.database.performInitialization();
|
||||
expect(logSpy).toHaveBeenCalledWith(
|
||||
jasmine.stringContaining('Unable to ensure uniqueness for auth data provider'),
|
||||
jasmine.anything()
|
||||
);
|
||||
});
|
||||
|
||||
it('should prevent concurrent signups with same anonymous authData', async () => {
|
||||
const anonymousId = 'anon-race-test-id';
|
||||
const authData = { anonymous: { id: anonymousId } };
|
||||
|
||||
const concurrentRequests = Array.from({ length: 5 }, () =>
|
||||
request({
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
url: 'http://localhost:8378/1/users',
|
||||
body: { authData },
|
||||
}).then(
|
||||
response => ({ success: true, data: response.data }),
|
||||
error => ({ success: false, error: error.data || error.message })
|
||||
)
|
||||
);
|
||||
|
||||
const results = await Promise.all(concurrentRequests);
|
||||
const successes = results.filter(r => r.success);
|
||||
const failures = results.filter(r => !r.success);
|
||||
|
||||
// All successes should reference the same user
|
||||
const uniqueObjectIds = new Set(successes.map(r => r.data.objectId));
|
||||
expect(uniqueObjectIds.size).toBe(1);
|
||||
|
||||
// Failures should be "this auth is already used" errors
|
||||
for (const failure of failures) {
|
||||
expect(failure.error.code).toBe(208);
|
||||
expect(failure.error.error).toBe('this auth is already used');
|
||||
}
|
||||
|
||||
// Verify only one user exists in the database with this authData
|
||||
const query = new Parse.Query('_User');
|
||||
query.equalTo('authData.anonymous.id', anonymousId);
|
||||
const users = await query.find({ useMasterKey: true });
|
||||
expect(users.length).toBe(1);
|
||||
});
|
||||
});
|
||||
+1164
-545
File diff suppressed because it is too large
Load Diff
@@ -355,20 +355,30 @@ describe('Auth Adapter features', () => {
|
||||
const authData = user.get('authData').modernAdapter3;
|
||||
expect(authData).toEqual({ foo: 'bar' });
|
||||
for (const call of afterSpy.calls.all()) {
|
||||
const args = call.args[2];
|
||||
const args = call.args[0];
|
||||
if (args.user) {
|
||||
user._objCount = args.user._objCount;
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(afterSpy).toHaveBeenCalledWith(
|
||||
{ id: 'modernAdapter3Data' },
|
||||
undefined,
|
||||
{ ip: '127.0.0.1', user, master: false },
|
||||
{ id: 'modernAdapter3Data' },
|
||||
undefined
|
||||
);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw if no triggers found', async () => {
|
||||
await reconfigureServer({ auth: { wrongAdapter } });
|
||||
const user = new Parse.User();
|
||||
await expectAsync(
|
||||
user.save({ authData: { wrongAdapter: { id: 'wrongAdapter' } } })
|
||||
).toBeRejectedWithError(
|
||||
'Adapter is not configured. Implement either validateAuthData or all of the following: validateSetUp, validateLogin and validateUpdate'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if policy does not match one of default/solo/additional', async () => {
|
||||
const adapterWithBadPolicy = {
|
||||
validateAppId: () => Promise.resolve(),
|
||||
@@ -477,33 +487,6 @@ describe('Auth Adapter features', () => {
|
||||
expect(baseAdapter2.validateAuthData).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should not perform authData validation twice when data mutated', async () => {
|
||||
spyOn(baseAdapter, 'validateAuthData').and.resolveTo({});
|
||||
await reconfigureServer({
|
||||
auth: { baseAdapter },
|
||||
allowExpiredAuthDataToken: false,
|
||||
});
|
||||
|
||||
const user = new Parse.User();
|
||||
|
||||
await user.save({
|
||||
authData: {
|
||||
baseAdapter: { id: 'baseAdapter', token: "sometoken1" },
|
||||
},
|
||||
});
|
||||
|
||||
expect(baseAdapter.validateAuthData).toHaveBeenCalledTimes(1);
|
||||
|
||||
const user2 = new Parse.User();
|
||||
await user2.save({
|
||||
authData: {
|
||||
baseAdapter: { id: 'baseAdapter', token: "sometoken2" },
|
||||
},
|
||||
});
|
||||
|
||||
expect(baseAdapter.validateAuthData).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should require additional provider if configured', async () => {
|
||||
await reconfigureServer({
|
||||
auth: { baseAdapter, additionalAdapter },
|
||||
|
||||
+54
-64
@@ -1,5 +1,5 @@
|
||||
'use strict';
|
||||
let commander;
|
||||
const commander = require('../lib/cli/utils/commander').default;
|
||||
const definitions = require('../lib/cli/definitions/parse-server').default;
|
||||
const liveQueryDefinitions = require('../lib/cli/definitions/parse-live-query-server').default;
|
||||
const path = require('path');
|
||||
@@ -28,12 +28,6 @@ const testDefinitions = {
|
||||
};
|
||||
|
||||
describe('commander additions', () => {
|
||||
beforeEach(() => {
|
||||
const command = require('../lib/cli/utils/commander').default;
|
||||
commander = new command.constructor();
|
||||
commander.storeOptionsAsProperties();
|
||||
commander.allowExcessArguments();
|
||||
});
|
||||
afterEach(done => {
|
||||
commander.options = [];
|
||||
delete commander.arg0;
|
||||
@@ -213,36 +207,6 @@ describe('LiveQuery definitions', () => {
|
||||
describe('execution', () => {
|
||||
const binPath = path.resolve(__dirname, '../bin/parse-server');
|
||||
let childProcess;
|
||||
let aggregatedData;
|
||||
|
||||
function handleStdout(childProcess, done, aggregatedData, requiredData) {
|
||||
childProcess.stdout.on('data', data => {
|
||||
data = data.toString();
|
||||
aggregatedData.push(data);
|
||||
if (requiredData.every(required => aggregatedData.some(aggregated => aggregated.includes(required)))) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleStderr(childProcess, done) {
|
||||
childProcess.stderr.on('data', data => {
|
||||
data = data.toString();
|
||||
if (!data.includes('[DEP0040] DeprecationWarning')) {
|
||||
done.fail(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleError(childProcess, done) {
|
||||
childProcess.on('error', err => {
|
||||
done.fail(err);
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
aggregatedData = [];
|
||||
});
|
||||
|
||||
afterEach(done => {
|
||||
if (childProcess) {
|
||||
@@ -254,22 +218,28 @@ describe('execution', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it_id('a0ab74b4-f805-4e03-b31d-b5cd59e64495')(it)('should start Parse Server', done => {
|
||||
it('should start Parse Server', done => {
|
||||
const env = { ...process.env };
|
||||
env.NODE_OPTIONS = '--dns-result-order=ipv4first --trace-deprecation';
|
||||
env.NODE_OPTIONS = '--dns-result-order=ipv4first';
|
||||
childProcess = spawn(
|
||||
binPath,
|
||||
['--appId', 'test', '--masterKey', 'test', '--databaseURI', databaseURI, '--port', '1339'],
|
||||
{ env }
|
||||
);
|
||||
handleStdout(childProcess, done, aggregatedData, ['parse-server running on']);
|
||||
handleStderr(childProcess, done);
|
||||
handleError(childProcess, done);
|
||||
childProcess.stdout.on('data', data => {
|
||||
data = data.toString();
|
||||
if (data.includes('parse-server running on')) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
childProcess.stderr.on('data', data => {
|
||||
done.fail(data.toString());
|
||||
});
|
||||
});
|
||||
|
||||
it_id('d7165081-b133-4cba-901b-19128ce41301')(it)('should start Parse Server with GraphQL', async done => {
|
||||
it('should start Parse Server with GraphQL', async done => {
|
||||
const env = { ...process.env };
|
||||
env.NODE_OPTIONS = '--dns-result-order=ipv4first --trace-deprecation';
|
||||
env.NODE_OPTIONS = '--dns-result-order=ipv4first';
|
||||
childProcess = spawn(
|
||||
binPath,
|
||||
[
|
||||
@@ -285,17 +255,23 @@ describe('execution', () => {
|
||||
],
|
||||
{ env }
|
||||
);
|
||||
handleStdout(childProcess, done, aggregatedData, [
|
||||
'parse-server running on',
|
||||
'GraphQL running on',
|
||||
]);
|
||||
handleStderr(childProcess, done);
|
||||
handleError(childProcess, done);
|
||||
let output = '';
|
||||
childProcess.stdout.on('data', data => {
|
||||
data = data.toString();
|
||||
output += data;
|
||||
if (data.includes('GraphQL running on')) {
|
||||
expect(output).toMatch('parse-server running on');
|
||||
done();
|
||||
}
|
||||
});
|
||||
childProcess.stderr.on('data', data => {
|
||||
done.fail(data.toString());
|
||||
});
|
||||
});
|
||||
|
||||
it_id('2769cdb4-ce8a-484d-8a91-635b5894ba7e')(it)('should start Parse Server with GraphQL and Playground', async done => {
|
||||
it('should start Parse Server with GraphQL and Playground', async done => {
|
||||
const env = { ...process.env };
|
||||
env.NODE_OPTIONS = '--dns-result-order=ipv4first --trace-deprecation';
|
||||
env.NODE_OPTIONS = '--dns-result-order=ipv4first';
|
||||
childProcess = spawn(
|
||||
binPath,
|
||||
[
|
||||
@@ -312,25 +288,39 @@ describe('execution', () => {
|
||||
],
|
||||
{ env }
|
||||
);
|
||||
handleStdout(childProcess, done, aggregatedData, [
|
||||
'parse-server running on',
|
||||
'Playground running on',
|
||||
'GraphQL running on',
|
||||
]);
|
||||
handleStderr(childProcess, done);
|
||||
handleError(childProcess, done);
|
||||
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());
|
||||
});
|
||||
});
|
||||
|
||||
it_id('23caddd7-bfea-4869-8bd4-0f2cd283c8bd')(it)('can start Parse Server with auth via CLI', done => {
|
||||
it('can start Parse Server with auth via CLI', done => {
|
||||
const env = { ...process.env };
|
||||
env.NODE_OPTIONS = '--dns-result-order=ipv4first --trace-deprecation';
|
||||
env.NODE_OPTIONS = '--dns-result-order=ipv4first';
|
||||
childProcess = spawn(
|
||||
binPath,
|
||||
['--databaseURI', databaseURI, './spec/configs/CLIConfigAuth.json'],
|
||||
{ env }
|
||||
);
|
||||
handleStdout(childProcess, done, aggregatedData, ['parse-server running on']);
|
||||
handleStderr(childProcess, done);
|
||||
handleError(childProcess, done);
|
||||
childProcess.stdout.on('data', data => {
|
||||
data = data.toString();
|
||||
console.log(data);
|
||||
if (data.includes('parse-server running on')) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
childProcess.stderr.on('data', data => {
|
||||
data = data.toString();
|
||||
done.fail(data.toString());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,9 +6,6 @@ const validatorFail = () => {
|
||||
const validatorSuccess = () => {
|
||||
return true;
|
||||
};
|
||||
function testConfig() {
|
||||
return Parse.Config.save({ internal: 'i', string: 's', number: 12 }, { internal: true });
|
||||
}
|
||||
|
||||
describe('cloud validator', () => {
|
||||
it('complete validator', async done => {
|
||||
@@ -197,6 +194,27 @@ describe('cloud validator', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('set params on cloud functions', done => {
|
||||
Parse.Cloud.define(
|
||||
'hello',
|
||||
() => {
|
||||
return 'Hello world!';
|
||||
},
|
||||
{
|
||||
fields: ['a'],
|
||||
}
|
||||
);
|
||||
Parse.Cloud.run('hello', {})
|
||||
.then(() => {
|
||||
fail('function should have failed.');
|
||||
})
|
||||
.catch(error => {
|
||||
expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR);
|
||||
expect(error.message).toEqual('Validation failed. Please specify data for a.');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('allow params on cloud functions', done => {
|
||||
Parse.Cloud.define(
|
||||
'hello',
|
||||
@@ -734,38 +752,6 @@ describe('cloud validator', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it_id('893eec0c-41bd-4adf-8f0a-306087ad8d61')(it)('basic beforeSave Parse.Config skipWithMasterKey', async () => {
|
||||
Parse.Cloud.beforeSave(
|
||||
Parse.Config,
|
||||
() => {
|
||||
throw 'beforeSaveFile should have resolved using master key.';
|
||||
},
|
||||
{
|
||||
skipWithMasterKey: true,
|
||||
}
|
||||
);
|
||||
const config = await testConfig();
|
||||
expect(config.get('internal')).toBe('i');
|
||||
expect(config.get('string')).toBe('s');
|
||||
expect(config.get('number')).toBe(12);
|
||||
});
|
||||
|
||||
it_id('91e739a4-6a38-405c-8f83-f36d48220734')(it)('basic afterSave Parse.Config skipWithMasterKey', async () => {
|
||||
Parse.Cloud.afterSave(
|
||||
Parse.Config,
|
||||
() => {
|
||||
throw 'beforeSaveFile should have resolved using master key.';
|
||||
},
|
||||
{
|
||||
skipWithMasterKey: true,
|
||||
}
|
||||
);
|
||||
const config = await testConfig();
|
||||
expect(config.get('internal')).toBe('i');
|
||||
expect(config.get('string')).toBe('s');
|
||||
expect(config.get('number')).toBe(12);
|
||||
});
|
||||
|
||||
it('beforeSave validateMasterKey and skipWithMasterKey fail', async function (done) {
|
||||
Parse.Cloud.beforeSave(
|
||||
'BeforeSave',
|
||||
@@ -1476,7 +1462,7 @@ describe('cloud validator', () => {
|
||||
});
|
||||
|
||||
it('validate afterSaveFile fail', async done => {
|
||||
Parse.Cloud.afterSave(Parse.File, () => {}, validatorFail);
|
||||
Parse.Cloud.beforeSave(Parse.File, () => {}, validatorFail);
|
||||
try {
|
||||
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
|
||||
await file.save({ useMasterKey: true });
|
||||
@@ -1531,42 +1517,6 @@ describe('cloud validator', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it_id('32ca1a99-7f2b-429d-a7cf-62b6661d0af6')(it)('validate beforeSave Parse.Config', async () => {
|
||||
Parse.Cloud.beforeSave(Parse.Config, () => {}, validatorSuccess);
|
||||
const config = await testConfig();
|
||||
expect(config.get('internal')).toBe('i');
|
||||
expect(config.get('string')).toBe('s');
|
||||
expect(config.get('number')).toBe(12);
|
||||
});
|
||||
|
||||
it_id('c84d11e7-d09c-4843-ad98-f671511bf612')(it)('validate beforeSave Parse.Config fail', async () => {
|
||||
Parse.Cloud.beforeSave(Parse.Config, () => {}, validatorFail);
|
||||
try {
|
||||
await testConfig();
|
||||
fail('cloud function should have failed.');
|
||||
} catch (e) {
|
||||
expect(e.code).toBe(Parse.Error.VALIDATION_ERROR);
|
||||
}
|
||||
});
|
||||
|
||||
it_id('b18b9a6a-0e35-4b60-9771-30f53501df3c')(it)('validate afterSave Parse.Config', async () => {
|
||||
Parse.Cloud.afterSave(Parse.Config, () => {}, validatorSuccess);
|
||||
const config = await testConfig();
|
||||
expect(config.get('internal')).toBe('i');
|
||||
expect(config.get('string')).toBe('s');
|
||||
expect(config.get('number')).toBe(12);
|
||||
});
|
||||
|
||||
it_id('ef761222-1758-4614-b984-da84d73fc10c')(it)('validate afterSave Parse.Config fail', async () => {
|
||||
Parse.Cloud.afterSave(Parse.Config, () => {}, validatorFail);
|
||||
try {
|
||||
await testConfig();
|
||||
fail('cloud function should have failed.');
|
||||
} catch (e) {
|
||||
expect(e.code).toBe(Parse.Error.VALIDATION_ERROR);
|
||||
}
|
||||
});
|
||||
|
||||
it('Should have validator', async done => {
|
||||
Parse.Cloud.define(
|
||||
'myFunction',
|
||||
@@ -1679,7 +1629,7 @@ describe('cloud validator', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('Logs on multiple invalid configs', () => {
|
||||
it('Logs on invalid config', () => {
|
||||
const fields = [
|
||||
{
|
||||
field: 'otherKey',
|
||||
|
||||
+95
-927
File diff suppressed because it is too large
Load Diff
@@ -15,17 +15,10 @@ describe('Cloud Code Logger', () => {
|
||||
// useful to flip to false for fine tuning :).
|
||||
silent: true,
|
||||
logLevel: undefined,
|
||||
logLevels: {
|
||||
cloudFunctionError: 'error',
|
||||
cloudFunctionSuccess: 'info',
|
||||
triggerAfter: 'info',
|
||||
triggerBeforeError: 'error',
|
||||
triggerBeforeSuccess: 'info',
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
return Parse.User.signUp('tester', 'abc')
|
||||
.catch(() => { })
|
||||
.catch(() => {})
|
||||
.then(loggedInUser => (user = loggedInUser))
|
||||
.then(() => Parse.User.logIn(user.get('username'), 'abc'));
|
||||
})
|
||||
@@ -37,7 +30,7 @@ describe('Cloud Code Logger', () => {
|
||||
// Note that helpers takes care of logout.
|
||||
// see helpers.js:afterEach
|
||||
|
||||
it_id('02d53b97-3ec7-46fb-abb6-176fd6e85590')(it)('should expose log to functions', () => {
|
||||
it('should expose log to functions', () => {
|
||||
const spy = spyOn(Config.get('test').loggerController, 'log').and.callThrough();
|
||||
Parse.Cloud.define('loggerTest', req => {
|
||||
req.log.info('logTest', 'info log', { info: 'some log' });
|
||||
@@ -67,7 +60,7 @@ describe('Cloud Code Logger', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_id('768412f5-d32f-4134-89a6-08949781a6c0')(it)('trigger should obfuscate password', done => {
|
||||
it('trigger should obfuscate password', done => {
|
||||
Parse.Cloud.beforeSave(Parse.User, req => {
|
||||
return req.object;
|
||||
});
|
||||
@@ -82,7 +75,7 @@ describe('Cloud Code Logger', () => {
|
||||
.then(null, e => done.fail(e));
|
||||
});
|
||||
|
||||
it_id('3c394047-272e-4728-9d02-9eaa660d2ed2')(it)('should expose log to trigger', done => {
|
||||
it('should expose log to trigger', done => {
|
||||
Parse.Cloud.beforeSave('MyObject', req => {
|
||||
req.log.info('beforeSave MyObject', 'info log', { info: 'some log' });
|
||||
req.log.error('beforeSave MyObject', 'error log', {
|
||||
@@ -120,7 +113,7 @@ describe('Cloud Code Logger', () => {
|
||||
expect(truncatedString.length).toBe(1015); // truncate length + the string '... (truncated)'
|
||||
});
|
||||
|
||||
it_id('4a009b1f-9203-49ca-8d48-5b45f4eedbdf')(it)('should truncate input and result of long lines', done => {
|
||||
it('should truncate input and result of long lines', done => {
|
||||
const longString = fs.readFileSync(loremFile, 'utf8');
|
||||
Parse.Cloud.define('aFunction', req => {
|
||||
return req.params;
|
||||
@@ -138,8 +131,8 @@ describe('Cloud Code Logger', () => {
|
||||
.then(null, e => done.fail(e));
|
||||
});
|
||||
|
||||
it_id('9857e15d-bb18-478d-8a67-fdaad3e89565')(it)('should log an afterSave', done => {
|
||||
Parse.Cloud.afterSave('MyObject', () => { });
|
||||
it('should log an afterSave', done => {
|
||||
Parse.Cloud.afterSave('MyObject', () => {});
|
||||
new Parse.Object('MyObject')
|
||||
.save()
|
||||
.then(() => {
|
||||
@@ -151,7 +144,7 @@ describe('Cloud Code Logger', () => {
|
||||
.then(null, e => done.fail(e));
|
||||
});
|
||||
|
||||
it_id('ec13a296-f8b1-4fc6-985a-3593462edd9c')(it)('should log a denied beforeSave', done => {
|
||||
it('should log a denied beforeSave', done => {
|
||||
Parse.Cloud.beforeSave('MyObject', () => {
|
||||
throw 'uh oh!';
|
||||
});
|
||||
@@ -174,7 +167,7 @@ describe('Cloud Code Logger', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_id('3e0caa45-60d6-41af-829a-fd389710c132')(it)('should log cloud function success', done => {
|
||||
it('should log cloud function success', done => {
|
||||
Parse.Cloud.define('aFunction', () => {
|
||||
return 'it worked!';
|
||||
});
|
||||
@@ -189,7 +182,7 @@ describe('Cloud Code Logger', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_id('8088de8a-7cba-4035-8b05-4a903307e674')(it)('should log cloud function execution using the custom log level', async () => {
|
||||
it('should log cloud function execution using the custom log level', async done => {
|
||||
Parse.Cloud.define('aFunction', () => {
|
||||
return 'it worked!';
|
||||
});
|
||||
@@ -203,7 +196,6 @@ describe('Cloud Code Logger', () => {
|
||||
expect(log).toEqual('info');
|
||||
});
|
||||
|
||||
Parse.Cloud._removeAllHooks();
|
||||
await reconfigureServer({
|
||||
silent: true,
|
||||
logLevels: {
|
||||
@@ -212,10 +204,6 @@ describe('Cloud Code Logger', () => {
|
||||
},
|
||||
});
|
||||
|
||||
Parse.Cloud.define('bFunction', () => {
|
||||
throw new Error('Failed');
|
||||
});
|
||||
|
||||
spy = spyOn(Config.get('test').loggerController.adapter, 'log').and.callThrough();
|
||||
|
||||
try {
|
||||
@@ -226,12 +214,15 @@ describe('Cloud Code Logger', () => {
|
||||
.allArgs()
|
||||
.find(log => log[1].startsWith('Failed running cloud function bFunction for '))?.[0];
|
||||
expect(log).toEqual('info');
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('should log cloud function triggers using the custom log level', async () => {
|
||||
Parse.Cloud.beforeSave('TestClass', () => {});
|
||||
Parse.Cloud.afterSave('TestClass', () => {});
|
||||
|
||||
const execTest = async (logLevel, triggerBeforeSuccess, triggerAfter) => {
|
||||
Parse.Cloud._removeAllHooks();
|
||||
await reconfigureServer({
|
||||
silent: true,
|
||||
logLevel,
|
||||
@@ -241,9 +232,6 @@ describe('Cloud Code Logger', () => {
|
||||
},
|
||||
});
|
||||
|
||||
Parse.Cloud.beforeSave('TestClass', () => { });
|
||||
Parse.Cloud.afterSave('TestClass', () => { });
|
||||
|
||||
spy = spyOn(Config.get('test').loggerController.adapter, 'log').and.callThrough();
|
||||
const obj = new Parse.Object('TestClass');
|
||||
await obj.save();
|
||||
@@ -265,13 +253,13 @@ describe('Cloud Code Logger', () => {
|
||||
expect(calls).toEqual({ beforeSave: 'warn', afterSave: undefined });
|
||||
});
|
||||
|
||||
it_id('97e0eafa-cde6-4a9a-9e53-7db98bacbc62')(it)('should log cloud function failure', done => {
|
||||
it('should log cloud function failure', done => {
|
||||
Parse.Cloud.define('aFunction', () => {
|
||||
throw 'it failed!';
|
||||
});
|
||||
|
||||
Parse.Cloud.run('aFunction', { foo: 'bar' })
|
||||
.catch(() => { })
|
||||
.catch(() => {})
|
||||
.then(() => {
|
||||
const logs = spy.calls.all().reverse();
|
||||
expect(logs[0].args[1]).toBe('Parse error: ');
|
||||
@@ -292,7 +280,6 @@ describe('Cloud Code Logger', () => {
|
||||
});
|
||||
|
||||
xit('should log a changed beforeSave indicating a change', done => {
|
||||
pending('needs more work.....');
|
||||
const logController = new LoggerController(new WinstonLoggerAdapter());
|
||||
|
||||
Parse.Cloud.beforeSave('MyObject', req => {
|
||||
@@ -315,9 +302,9 @@ describe('Cloud Code Logger', () => {
|
||||
done();
|
||||
})
|
||||
.then(null, e => done.fail(JSON.stringify(e)));
|
||||
});
|
||||
}).pend('needs more work.....');
|
||||
|
||||
it_id('b86e8168-8370-4730-a4ba-24ca3016ad66')(it)('cloud function should obfuscate password', done => {
|
||||
it('cloud function should obfuscate password', done => {
|
||||
Parse.Cloud.define('testFunction', () => {
|
||||
return 'verify code success';
|
||||
});
|
||||
@@ -347,55 +334,4 @@ describe('Cloud Code Logger', () => {
|
||||
expect(args[0]).toBe('Parse error: ');
|
||||
expect(args[1].message).toBe('Object not found.');
|
||||
});
|
||||
|
||||
it('should log cloud function execution using the silent log level', async () => {
|
||||
Parse.Cloud._removeAllHooks();
|
||||
await reconfigureServer({
|
||||
logLevels: {
|
||||
cloudFunctionSuccess: 'silent',
|
||||
cloudFunctionError: 'silent',
|
||||
},
|
||||
});
|
||||
Parse.Cloud.define('aFunction', () => {
|
||||
return 'it worked!';
|
||||
});
|
||||
Parse.Cloud.define('bFunction', () => {
|
||||
throw new Error('Failed');
|
||||
});
|
||||
spy = spyOn(Config.get('test').loggerController.adapter, 'log').and.callThrough();
|
||||
|
||||
await Parse.Cloud.run('aFunction', { foo: 'bar' });
|
||||
expect(spy).toHaveBeenCalledTimes(0);
|
||||
|
||||
await expectAsync(Parse.Cloud.run('bFunction', { foo: 'bar' })).toBeRejected();
|
||||
// Not "Failed running cloud function message..."
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should log cloud function triggers using the silent log level', async () => {
|
||||
Parse.Cloud._removeAllHooks();
|
||||
await reconfigureServer({
|
||||
logLevels: {
|
||||
triggerAfter: 'silent',
|
||||
triggerBeforeSuccess: 'silent',
|
||||
triggerBeforeError: 'silent',
|
||||
},
|
||||
});
|
||||
Parse.Cloud.beforeSave('TestClassError', () => {
|
||||
throw new Error('Failed');
|
||||
});
|
||||
Parse.Cloud.beforeSave('TestClass', () => { });
|
||||
Parse.Cloud.afterSave('TestClass', () => { });
|
||||
|
||||
spy = spyOn(Config.get('test').loggerController.adapter, 'log').and.callThrough();
|
||||
|
||||
const obj = new Parse.Object('TestClass');
|
||||
await obj.save();
|
||||
expect(spy).toHaveBeenCalledTimes(0);
|
||||
|
||||
const objError = new Parse.Object('TestClassError');
|
||||
await expectAsync(objError.save()).toBeRejected();
|
||||
// Not "beforeSave failed for TestClassError for user ..."
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -413,13 +413,6 @@ describe('DatabaseController', function () {
|
||||
case_insensitive_username: { username: 1 },
|
||||
case_insensitive_email: { email: 1 },
|
||||
email_1: { email: 1 },
|
||||
_email_verify_token: { _email_verify_token: 1 },
|
||||
_perishable_token: { _perishable_token: 1 },
|
||||
_auth_data_custom_id: { '_auth_data_custom.id': 1 },
|
||||
_auth_data_facebook_id: { '_auth_data_facebook.id': 1 },
|
||||
_auth_data_myoauth_id: { '_auth_data_myoauth.id': 1 },
|
||||
_auth_data_shortLivedAuth_id: { '_auth_data_shortLivedAuth.id': 1 },
|
||||
_auth_data_anonymous_id: { '_auth_data_anonymous.id': 1 },
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -444,158 +437,9 @@ describe('DatabaseController', function () {
|
||||
_id_: { _id: 1 },
|
||||
username_1: { username: 1 },
|
||||
email_1: { email: 1 },
|
||||
_email_verify_token: { _email_verify_token: 1 },
|
||||
_perishable_token: { _perishable_token: 1 },
|
||||
_auth_data_custom_id: { '_auth_data_custom.id': 1 },
|
||||
_auth_data_facebook_id: { '_auth_data_facebook.id': 1 },
|
||||
_auth_data_myoauth_id: { '_auth_data_myoauth.id': 1 },
|
||||
_auth_data_shortLivedAuth_id: { '_auth_data_shortLivedAuth.id': 1 },
|
||||
_auth_data_anonymous_id: { '_auth_data_anonymous.id': 1 },
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
it_only_db('mongo')(
|
||||
'should use _email_verify_token index in email verification',
|
||||
async () => {
|
||||
const TestUtils = require('../lib/TestUtils');
|
||||
let emailVerificationLink;
|
||||
const emailSentPromise = TestUtils.resolvingPromise();
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: options => {
|
||||
emailVerificationLink = options.link;
|
||||
emailSentPromise.resolve();
|
||||
},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {},
|
||||
};
|
||||
await reconfigureServer({
|
||||
databaseURI: 'mongodb://localhost:27017/testEmailVerifyTokenIndexStats',
|
||||
databaseAdapter: undefined,
|
||||
appName: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
});
|
||||
|
||||
// Create a user to trigger email verification
|
||||
const user = new Parse.User();
|
||||
user.setUsername('statsuser');
|
||||
user.setPassword('password');
|
||||
user.set('email', 'stats@example.com');
|
||||
await user.signUp();
|
||||
await emailSentPromise;
|
||||
|
||||
// Get index stats before the query
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const collection = await config.database.adapter._adaptiveCollection('_User');
|
||||
const statsBefore = await collection._mongoCollection.aggregate([
|
||||
{ $indexStats: {} },
|
||||
]).toArray();
|
||||
const emailVerifyIndexBefore = statsBefore.find(
|
||||
stat => stat.name === '_email_verify_token'
|
||||
);
|
||||
const accessesBefore = emailVerifyIndexBefore?.accesses?.ops || 0;
|
||||
|
||||
// Perform email verification (this should use the index)
|
||||
const request = require('../lib/request');
|
||||
await request({
|
||||
url: emailVerificationLink,
|
||||
followRedirects: false,
|
||||
});
|
||||
|
||||
// Get index stats after the query
|
||||
const statsAfter = await collection._mongoCollection.aggregate([
|
||||
{ $indexStats: {} },
|
||||
]).toArray();
|
||||
const emailVerifyIndexAfter = statsAfter.find(
|
||||
stat => stat.name === '_email_verify_token'
|
||||
);
|
||||
const accessesAfter = emailVerifyIndexAfter?.accesses?.ops || 0;
|
||||
|
||||
// Verify the index was actually used
|
||||
expect(accessesAfter).toBeGreaterThan(accessesBefore);
|
||||
expect(emailVerifyIndexAfter).toBeDefined();
|
||||
|
||||
// Verify email verification succeeded
|
||||
await user.fetch();
|
||||
expect(user.get('emailVerified')).toBe(true);
|
||||
}
|
||||
);
|
||||
|
||||
it_only_db('mongo')(
|
||||
'should use _perishable_token index in password reset',
|
||||
async () => {
|
||||
const TestUtils = require('../lib/TestUtils');
|
||||
let passwordResetLink;
|
||||
const emailSentPromise = TestUtils.resolvingPromise();
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: options => {
|
||||
passwordResetLink = options.link;
|
||||
emailSentPromise.resolve();
|
||||
},
|
||||
sendMail: () => {},
|
||||
};
|
||||
await reconfigureServer({
|
||||
databaseURI: 'mongodb://localhost:27017/testPerishableTokenIndexStats',
|
||||
databaseAdapter: undefined,
|
||||
appName: 'test',
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
});
|
||||
|
||||
// Create a user
|
||||
const user = new Parse.User();
|
||||
user.setUsername('statsuser2');
|
||||
user.setPassword('oldpassword');
|
||||
user.set('email', 'stats2@example.com');
|
||||
await user.signUp();
|
||||
|
||||
// Request password reset
|
||||
await Parse.User.requestPasswordReset('stats2@example.com');
|
||||
await emailSentPromise;
|
||||
|
||||
const url = new URL(passwordResetLink);
|
||||
const token = url.searchParams.get('token');
|
||||
|
||||
// Get index stats before the query
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const collection = await config.database.adapter._adaptiveCollection('_User');
|
||||
const statsBefore = await collection._mongoCollection.aggregate([
|
||||
{ $indexStats: {} },
|
||||
]).toArray();
|
||||
const perishableTokenIndexBefore = statsBefore.find(
|
||||
stat => stat.name === '_perishable_token'
|
||||
);
|
||||
const accessesBefore = perishableTokenIndexBefore?.accesses?.ops || 0;
|
||||
|
||||
// Perform password reset (this should use the index)
|
||||
const request = require('../lib/request');
|
||||
await request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: { new_password: 'newpassword', token, username: 'statsuser2' },
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
followRedirects: false,
|
||||
});
|
||||
|
||||
// Get index stats after the query
|
||||
const statsAfter = await collection._mongoCollection.aggregate([
|
||||
{ $indexStats: {} },
|
||||
]).toArray();
|
||||
const perishableTokenIndexAfter = statsAfter.find(
|
||||
stat => stat.name === '_perishable_token'
|
||||
);
|
||||
const accessesAfter = perishableTokenIndexAfter?.accesses?.ops || 0;
|
||||
|
||||
// Verify the index was actually used
|
||||
expect(accessesAfter).toBeGreaterThan(accessesBefore);
|
||||
expect(perishableTokenIndexAfter).toBeDefined();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('convertEmailToLowercase', () => {
|
||||
@@ -771,76 +615,6 @@ describe('DatabaseController', function () {
|
||||
expect(result2.length).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update with validateOnly', () => {
|
||||
const mockStorageAdapter = {
|
||||
findOneAndUpdate: () => Promise.resolve({}),
|
||||
find: () => Promise.resolve([{ objectId: 'test123', testField: 'initialValue' }]),
|
||||
watch: () => Promise.resolve(),
|
||||
getAllClasses: () =>
|
||||
Promise.resolve([
|
||||
{
|
||||
className: 'TestObject',
|
||||
fields: { testField: 'String' },
|
||||
indexes: {},
|
||||
classLevelPermissions: { protectedFields: {} },
|
||||
},
|
||||
]),
|
||||
};
|
||||
|
||||
it('should use primary readPreference when validateOnly is true', async () => {
|
||||
const databaseController = new DatabaseController(mockStorageAdapter, {});
|
||||
const findSpy = spyOn(mockStorageAdapter, 'find').and.callThrough();
|
||||
const findOneAndUpdateSpy = spyOn(mockStorageAdapter, 'findOneAndUpdate').and.callThrough();
|
||||
|
||||
try {
|
||||
// Call update with validateOnly: true (same as RestWrite.runBeforeSaveTrigger)
|
||||
await databaseController.update(
|
||||
'TestObject',
|
||||
{ objectId: 'test123' },
|
||||
{ testField: 'newValue' },
|
||||
{},
|
||||
true, // skipSanitization: true (matches RestWrite behavior)
|
||||
true // validateOnly: true
|
||||
);
|
||||
} catch (error) {
|
||||
// validateOnly may throw, but we're checking the find call options
|
||||
}
|
||||
|
||||
// Verify that find was called with primary readPreference
|
||||
expect(findSpy).toHaveBeenCalled();
|
||||
const findCall = findSpy.calls.mostRecent();
|
||||
expect(findCall.args[3]).toEqual({ readPreference: 'primary' }); // options parameter
|
||||
|
||||
// Verify that findOneAndUpdate was NOT called (only validation, no actual update)
|
||||
expect(findOneAndUpdateSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not use primary readPreference when validateOnly is false', async () => {
|
||||
const databaseController = new DatabaseController(mockStorageAdapter, {});
|
||||
const findSpy = spyOn(mockStorageAdapter, 'find').and.callThrough();
|
||||
const findOneAndUpdateSpy = spyOn(mockStorageAdapter, 'findOneAndUpdate').and.callThrough();
|
||||
|
||||
try {
|
||||
// Call update with validateOnly: false
|
||||
await databaseController.update(
|
||||
'TestObject',
|
||||
{ objectId: 'test123' },
|
||||
{ testField: 'newValue' },
|
||||
{},
|
||||
false, // skipSanitization
|
||||
false // validateOnly
|
||||
);
|
||||
} catch (error) {
|
||||
// May throw for other reasons, but we're checking the call pattern
|
||||
}
|
||||
|
||||
// When validateOnly is false, find should not be called for validation
|
||||
// Instead, findOneAndUpdate should be called
|
||||
expect(findSpy).not.toHaveBeenCalled();
|
||||
expect(findOneAndUpdateSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function buildCLP(pointerNames) {
|
||||
|
||||
@@ -371,7 +371,7 @@ describe('DefinedSchemas', () => {
|
||||
expect(schema.indexes).toEqual(indexes);
|
||||
});
|
||||
|
||||
it('should delete unknown indexes when keepUnknownIndexes is not set', async () => {
|
||||
it('should delete removed indexes', async () => {
|
||||
const server = await reconfigureServer();
|
||||
|
||||
let indexes = { complex: { createdAt: 1, updatedAt: 1 } };
|
||||
@@ -393,53 +393,6 @@ describe('DefinedSchemas', () => {
|
||||
cleanUpIndexes(schema);
|
||||
expect(schema.indexes).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should delete unknown indexes when keepUnknownIndexes is set to false', async () => {
|
||||
const server = await reconfigureServer();
|
||||
|
||||
let indexes = { complex: { createdAt: 1, updatedAt: 1 } };
|
||||
|
||||
let schemas = { definitions: [{ className: 'Test', indexes }], keepUnknownIndexes: false };
|
||||
await new DefinedSchemas(schemas, server.config).execute();
|
||||
|
||||
indexes = {};
|
||||
schemas = { definitions: [{ className: 'Test', indexes }], keepUnknownIndexes: false };
|
||||
// Change indexes
|
||||
await new DefinedSchemas(schemas, server.config).execute();
|
||||
let schema = await new Parse.Schema('Test').get();
|
||||
cleanUpIndexes(schema);
|
||||
expect(schema.indexes).toBeUndefined();
|
||||
|
||||
// Update
|
||||
await new DefinedSchemas(schemas, server.config).execute();
|
||||
schema = await new Parse.Schema('Test').get();
|
||||
cleanUpIndexes(schema);
|
||||
expect(schema.indexes).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not delete unknown indexes when keepUnknownIndexes is set to true', async () => {
|
||||
const server = await reconfigureServer();
|
||||
|
||||
const indexes = { complex: { createdAt: 1, updatedAt: 1 } };
|
||||
|
||||
let schemas = { definitions: [{ className: 'Test', indexes }], keepUnknownIndexes: true };
|
||||
await new DefinedSchemas(schemas, server.config).execute();
|
||||
|
||||
schemas = { definitions: [{ className: 'Test', indexes: {} }], keepUnknownIndexes: true };
|
||||
|
||||
// Change indexes
|
||||
await new DefinedSchemas(schemas, server.config).execute();
|
||||
let schema = await new Parse.Schema('Test').get();
|
||||
cleanUpIndexes(schema);
|
||||
expect(schema.indexes).toEqual({ complex: { createdAt: 1, updatedAt: 1 } });
|
||||
|
||||
// Update
|
||||
await new DefinedSchemas(schemas, server.config).execute();
|
||||
schema = await new Parse.Schema('Test').get();
|
||||
cleanUpIndexes(schema);
|
||||
expect(schema.indexes).toEqual(indexes);
|
||||
});
|
||||
|
||||
xit('should keep protected indexes', async () => {
|
||||
const server = await reconfigureServer();
|
||||
|
||||
@@ -689,8 +642,7 @@ describe('DefinedSchemas', () => {
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith(`Failed to run migrations: ${error.toString()}`);
|
||||
});
|
||||
|
||||
it_id('a18bf4f2-25c8-4de3-b986-19cb1ab163b8')(it)('should perform migration in parallel without failing', async () => {
|
||||
it('should perform migration in parallel without failing', async () => {
|
||||
const server = await reconfigureServer();
|
||||
const logger = require('../lib/logger').logger;
|
||||
spyOn(logger, 'error').and.callThrough();
|
||||
|
||||
@@ -45,29 +45,4 @@ describe('Deprecator', () => {
|
||||
`DeprecationWarning: ${options.usage} is deprecated and will be removed in a future version. ${options.solution}`
|
||||
);
|
||||
});
|
||||
|
||||
it('logs deprecation for nested option key with dot notation', async () => {
|
||||
deprecations = [{ optionKey: 'databaseOptions.allowPublicExplain', changeNewDefault: 'false' }];
|
||||
|
||||
spyOn(Deprecator, '_getDeprecations').and.callFake(() => deprecations);
|
||||
const logger = require('../lib/logger').logger;
|
||||
const logSpy = spyOn(logger, 'warn').and.callFake(() => {});
|
||||
|
||||
await reconfigureServer();
|
||||
expect(logSpy.calls.all()[0].args[0]).toEqual(
|
||||
`DeprecationWarning: The Parse Server option '${deprecations[0].optionKey}' default will change to '${deprecations[0].changeNewDefault}' in a future version.`
|
||||
);
|
||||
});
|
||||
|
||||
it('does not log deprecation for nested option key if option is set manually', async () => {
|
||||
deprecations = [{ optionKey: 'databaseOptions.allowPublicExplain', changeNewDefault: 'false' }];
|
||||
|
||||
spyOn(Deprecator, '_getDeprecations').and.callFake(() => deprecations);
|
||||
const logSpy = spyOn(Deprecator, '_logOption').and.callFake(() => {});
|
||||
const Config = require('../lib/Config');
|
||||
const config = Config.get('test');
|
||||
// Directly test scanParseServerOptions with nested option set
|
||||
Deprecator.scanParseServerOptions({ databaseOptions: { allowPublicExplain: true } });
|
||||
expect(logSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
+653
-616
File diff suppressed because it is too large
Load Diff
@@ -3,9 +3,11 @@ const request = require('../lib/request');
|
||||
describe('Enable express error handler', () => {
|
||||
it('should call the default handler in case of error, like updating a non existing object', async done => {
|
||||
spyOn(console, 'error');
|
||||
const parseServer = await reconfigureServer({
|
||||
enableExpressErrorHandler: true,
|
||||
});
|
||||
const parseServer = await reconfigureServer(
|
||||
Object.assign({}, defaultConfiguration, {
|
||||
enableExpressErrorHandler: true,
|
||||
})
|
||||
);
|
||||
parseServer.app.use(function (err, req, res, next) {
|
||||
expect(err.message).toBe('Object not found.');
|
||||
next(err);
|
||||
|
||||
@@ -21,14 +21,11 @@ const mockAdapter = {
|
||||
|
||||
// Small additional tests to improve overall coverage
|
||||
describe('FilesController', () => {
|
||||
it('should properly expand objects with sync getFileLocation', async () => {
|
||||
it('should properly expand objects', done => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
gridFSAdapter.getFileLocation = (config, filename) => {
|
||||
return config.mount + '/files/' + config.applicationId + '/' + encodeURIComponent(filename);
|
||||
}
|
||||
const filesController = new FilesController(gridFSAdapter);
|
||||
const result = await filesController.expandFilesInObject(config, function () { });
|
||||
const result = filesController.expandFilesInObject(config, function () {});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
|
||||
@@ -40,69 +37,12 @@ describe('FilesController', () => {
|
||||
const anObject = {
|
||||
aFile: fullFile,
|
||||
};
|
||||
await filesController.expandFilesInObject(config, anObject);
|
||||
filesController.expandFilesInObject(config, anObject);
|
||||
expect(anObject.aFile.url).toEqual('http://an.url');
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should properly expand objects with async getFileLocation', async () => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
gridFSAdapter.getFileLocation = async (config, filename) => {
|
||||
await Promise.resolve();
|
||||
return config.mount + '/files/' + config.applicationId + '/' + encodeURIComponent(filename);
|
||||
}
|
||||
const filesController = new FilesController(gridFSAdapter);
|
||||
const result = await filesController.expandFilesInObject(config, function () { });
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
|
||||
const fullFile = {
|
||||
type: '__type',
|
||||
url: 'http://an.url',
|
||||
};
|
||||
|
||||
const anObject = {
|
||||
aFile: fullFile,
|
||||
};
|
||||
await filesController.expandFilesInObject(config, anObject);
|
||||
expect(anObject.aFile.url).toEqual('http://an.url');
|
||||
});
|
||||
|
||||
it('should call getFileLocation when config.fileKey is undefined', async () => {
|
||||
const config = {};
|
||||
const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
|
||||
const fullFile = {
|
||||
name: 'mock-name',
|
||||
__type: 'File',
|
||||
};
|
||||
gridFSAdapter.getFileLocation = jasmine.createSpy('getFileLocation').and.returnValue(Promise.resolve('mock-url'));
|
||||
const filesController = new FilesController(gridFSAdapter);
|
||||
|
||||
const anObject = { aFile: fullFile };
|
||||
await filesController.expandFilesInObject(config, anObject);
|
||||
expect(gridFSAdapter.getFileLocation).toHaveBeenCalledWith(config, fullFile.name);
|
||||
expect(anObject.aFile.url).toEqual('mock-url');
|
||||
});
|
||||
|
||||
it('should call getFileLocation when config.fileKey is defined', async () => {
|
||||
const config = { fileKey: 'mock-key' };
|
||||
const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
|
||||
const fullFile = {
|
||||
name: 'mock-name',
|
||||
__type: 'File',
|
||||
};
|
||||
gridFSAdapter.getFileLocation = jasmine.createSpy('getFileLocation').and.returnValue(Promise.resolve('mock-url'));
|
||||
const filesController = new FilesController(gridFSAdapter);
|
||||
|
||||
const anObject = { aFile: fullFile };
|
||||
await filesController.expandFilesInObject(config, anObject);
|
||||
expect(gridFSAdapter.getFileLocation).toHaveBeenCalledWith(config, fullFile.name);
|
||||
expect(anObject.aFile.url).toEqual('mock-url');
|
||||
});
|
||||
|
||||
|
||||
it_only_db('mongo')('should pass databaseOptions to GridFSBucketAdapter', async () => {
|
||||
await reconfigureServer({
|
||||
databaseURI: 'mongodb://localhost:27017/parse',
|
||||
@@ -161,7 +101,7 @@ describe('FilesController', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should add a unique hash to the file name when the preserveFileName option is false', async () => {
|
||||
it('should add a unique hash to the file name when the preserveFileName option is false', done => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
spyOn(gridFSAdapter, 'createFile');
|
||||
@@ -172,15 +112,17 @@ describe('FilesController', () => {
|
||||
preserveFileName: false,
|
||||
});
|
||||
|
||||
await filesController.createFile(config, fileName);
|
||||
filesController.createFile(config, fileName);
|
||||
|
||||
expect(gridFSAdapter.createFile).toHaveBeenCalledTimes(1);
|
||||
expect(gridFSAdapter.createFile.calls.mostRecent().args[0]).toMatch(
|
||||
`^.{32}_${regexEscapedFileName}$`
|
||||
);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should not add a unique hash to the file name when the preserveFileName option is true', async () => {
|
||||
it('should not add a unique hash to the file name when the preserveFileName option is true', done => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
spyOn(gridFSAdapter, 'createFile');
|
||||
@@ -190,10 +132,12 @@ describe('FilesController', () => {
|
||||
preserveFileName: true,
|
||||
});
|
||||
|
||||
await filesController.createFile(config, fileName);
|
||||
filesController.createFile(config, fileName);
|
||||
|
||||
expect(gridFSAdapter.createFile).toHaveBeenCalledTimes(1);
|
||||
expect(gridFSAdapter.createFile.calls.mostRecent().args[0]).toEqual(fileName);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should handle adapter without getMetadata', async () => {
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const http = require('http');
|
||||
const express = require('express');
|
||||
const fetch = (...args) => import('node-fetch').then(({ default: fetch }) => fetch(...args));
|
||||
require('./helper');
|
||||
const { ParseGraphQLServer } = require('../lib/GraphQL/ParseGraphQLServer');
|
||||
|
||||
describe('graphql query complexity', () => {
|
||||
let httpServer;
|
||||
let graphQLServer;
|
||||
const headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Javascript-Key': 'test',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
async function setupGraphQL(serverOptions = {}) {
|
||||
if (httpServer) {
|
||||
await new Promise(resolve => httpServer.close(resolve));
|
||||
}
|
||||
const server = await reconfigureServer(serverOptions);
|
||||
const expressApp = express();
|
||||
httpServer = http.createServer(expressApp);
|
||||
expressApp.use('/parse', server.app);
|
||||
graphQLServer = new ParseGraphQLServer(server, {
|
||||
graphQLPath: '/graphql',
|
||||
});
|
||||
graphQLServer.applyGraphQL(expressApp);
|
||||
await new Promise(resolve => httpServer.listen({ port: 13378 }, resolve));
|
||||
}
|
||||
|
||||
async function graphqlRequest(query, requestHeaders = headers) {
|
||||
const response = await fetch('http://localhost:13378/graphql', {
|
||||
method: 'POST',
|
||||
headers: requestHeaders,
|
||||
body: JSON.stringify({ query }),
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// Returns a query with depth 4: users(1) > edges(2) > node(3) > objectId(4)
|
||||
function buildDeepQuery() {
|
||||
return '{ users { edges { node { objectId } } } }';
|
||||
}
|
||||
|
||||
function buildWideQuery(fieldCount) {
|
||||
const fields = Array.from({ length: fieldCount }, (_, i) => `field${i}: objectId`).join('\n ');
|
||||
return `{ users { edges { node { ${fields} } } } }`;
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
if (httpServer) {
|
||||
await new Promise(resolve => httpServer.close(resolve));
|
||||
httpServer = null;
|
||||
}
|
||||
});
|
||||
|
||||
describe('depth limit', () => {
|
||||
it('should reject query exceeding depth limit', async () => {
|
||||
await setupGraphQL({
|
||||
requestComplexity: { graphQLDepth: 3 },
|
||||
});
|
||||
const result = await graphqlRequest(buildDeepQuery());
|
||||
expect(result.errors).toBeDefined();
|
||||
expect(result.errors[0].message).toMatch(
|
||||
/GraphQL query depth of \d+ exceeds maximum allowed depth of 3/
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow query within depth limit', async () => {
|
||||
await setupGraphQL({
|
||||
requestComplexity: { graphQLDepth: 10 },
|
||||
});
|
||||
const result = await graphqlRequest(buildDeepQuery());
|
||||
expect(result.errors).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should allow deep query with master key', async () => {
|
||||
await setupGraphQL({
|
||||
requestComplexity: { graphQLDepth: 3 },
|
||||
});
|
||||
const result = await graphqlRequest(buildDeepQuery(), {
|
||||
...headers,
|
||||
'X-Parse-Master-Key': 'test',
|
||||
});
|
||||
expect(result.errors).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should allow unlimited depth when graphQLDepth is -1', async () => {
|
||||
await setupGraphQL({
|
||||
requestComplexity: { graphQLDepth: -1 },
|
||||
});
|
||||
const result = await graphqlRequest(buildDeepQuery());
|
||||
expect(result.errors).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('fields limit', () => {
|
||||
it('should reject query exceeding fields limit', async () => {
|
||||
await setupGraphQL({
|
||||
requestComplexity: { graphQLFields: 5 },
|
||||
});
|
||||
const result = await graphqlRequest(buildWideQuery(10));
|
||||
expect(result.errors).toBeDefined();
|
||||
expect(result.errors[0].message).toMatch(
|
||||
/Number of GraphQL fields \(\d+\) exceeds maximum allowed \(5\)/
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow query within fields limit', async () => {
|
||||
await setupGraphQL({
|
||||
requestComplexity: { graphQLFields: 200 },
|
||||
});
|
||||
const result = await graphqlRequest(buildDeepQuery());
|
||||
expect(result.errors).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should allow wide query with master key', async () => {
|
||||
await setupGraphQL({
|
||||
requestComplexity: { graphQLFields: 5 },
|
||||
});
|
||||
const result = await graphqlRequest(buildWideQuery(10), {
|
||||
...headers,
|
||||
'X-Parse-Master-Key': 'test',
|
||||
});
|
||||
expect(result.errors).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should count fragment fields at each spread location', async () => {
|
||||
// With correct counting: 2 aliases (2) + 2×edges (2) + 2×node (2) + 2×objectId from fragment (2) = 8
|
||||
// With incorrect counting (fragment once): 2 + 2 + 2 + 1 = 7
|
||||
// Set limit to 7 so incorrect counting passes but correct counting rejects
|
||||
await setupGraphQL({
|
||||
requestComplexity: { graphQLFields: 7 },
|
||||
});
|
||||
const result = await graphqlRequest(`
|
||||
fragment UserFields on User { objectId }
|
||||
{
|
||||
a1: users { edges { node { ...UserFields } } }
|
||||
a2: users { edges { node { ...UserFields } } }
|
||||
}
|
||||
`);
|
||||
expect(result.errors).toBeDefined();
|
||||
expect(result.errors[0].message).toMatch(
|
||||
/Number of GraphQL fields \(\d+\) exceeds maximum allowed \(7\)/
|
||||
);
|
||||
});
|
||||
|
||||
it('should count inline fragment fields toward depth and field limits', async () => {
|
||||
await setupGraphQL({
|
||||
requestComplexity: { graphQLFields: 3 },
|
||||
});
|
||||
// Inline fragment adds fields without increasing depth:
|
||||
// users(1) > edges(2) > ... on UserConnection { edges(3) > node(4) }
|
||||
const result = await graphqlRequest(`{
|
||||
users {
|
||||
edges {
|
||||
... on UserEdge {
|
||||
node {
|
||||
objectId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`);
|
||||
expect(result.errors).toBeDefined();
|
||||
expect(result.errors[0].message).toMatch(
|
||||
/Number of GraphQL fields \(\d+\) exceeds maximum allowed \(3\)/
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow unlimited fields when graphQLFields is -1', async () => {
|
||||
await setupGraphQL({
|
||||
requestComplexity: { graphQLFields: -1 },
|
||||
});
|
||||
const result = await graphqlRequest(buildWideQuery(50));
|
||||
expect(result.errors).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -24,20 +24,10 @@ describe_only_db('mongo')('GridFSBucket', () => {
|
||||
const databaseURI = 'mongodb://localhost:27017/parse';
|
||||
const gfsAdapter = new GridFSBucketAdapter(databaseURI, {
|
||||
retryWrites: true,
|
||||
// Parse Server-specific options that should be filtered out before passing to MongoDB client
|
||||
allowPublicExplain: true,
|
||||
// these are not supported by the mongo client
|
||||
enableSchemaHooks: true,
|
||||
schemaCacheTtl: 5000,
|
||||
maxTimeMS: 30000,
|
||||
disableIndexFieldValidation: true,
|
||||
logClientEvents: [{ name: 'commandStarted' }],
|
||||
createIndexUserUsername: true,
|
||||
createIndexUserUsernameCaseInsensitive: true,
|
||||
createIndexUserEmail: true,
|
||||
createIndexUserEmailCaseInsensitive: true,
|
||||
createIndexUserEmailVerifyToken: true,
|
||||
createIndexUserPasswordResetToken: true,
|
||||
createIndexRoleName: true,
|
||||
});
|
||||
|
||||
const db = await gfsAdapter._connect();
|
||||
@@ -431,14 +421,6 @@ describe_only_db('mongo')('GridFSBucket', () => {
|
||||
expect(gfsResult.toString('utf8')).toBe(twoMegabytesFile);
|
||||
});
|
||||
|
||||
it('properly upload a file when disableIndexFieldValidation exist in databaseOptions', async () => {
|
||||
const gfsAdapter = new GridFSBucketAdapter(databaseURI, { disableIndexFieldValidation: true });
|
||||
const twoMegabytesFile = randomString(2048 * 1024);
|
||||
await gfsAdapter.createFile('myFileName', twoMegabytesFile);
|
||||
const gfsResult = await gfsAdapter.getFileData('myFileName');
|
||||
expect(gfsResult.toString('utf8')).toBe(twoMegabytesFile);
|
||||
});
|
||||
|
||||
it('properly deletes a file from GridFS', async () => {
|
||||
const gfsAdapter = new GridFSBucketAdapter(databaseURI);
|
||||
await gfsAdapter.createFile('myFileName', 'a simple file');
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
const httpRequest = require('../lib/request'),
|
||||
HTTPResponse = require('../lib/request').HTTPResponse,
|
||||
bodyParser = require('body-parser'),
|
||||
express = require('express');
|
||||
|
||||
const port = 13371;
|
||||
@@ -9,7 +10,7 @@ const httpRequestServer = `http://localhost:${port}`;
|
||||
|
||||
function startServer(done) {
|
||||
const app = express();
|
||||
app.use(express.json({ type: '*/*' }));
|
||||
app.use(bodyParser.json({ type: '*/*' }));
|
||||
app.get('/hello', function (req, res) {
|
||||
res.json({ response: 'OK' });
|
||||
});
|
||||
|
||||
@@ -45,12 +45,8 @@ describe('Idempotency', () => {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = process.env.PARSE_SERVER_TEST_TIMEOUT || 10000;
|
||||
});
|
||||
|
||||
// Tests
|
||||
it_id('e25955fd-92eb-4b22-b8b7-38980e5cb223')(it)('should enforce idempotency for cloud code function', async () => {
|
||||
it('should enforce idempotency for cloud code function', async () => {
|
||||
let counter = 0;
|
||||
Parse.Cloud.define('myFunction', () => {
|
||||
counter++;
|
||||
@@ -73,7 +69,7 @@ describe('Idempotency', () => {
|
||||
expect(counter).toBe(1);
|
||||
});
|
||||
|
||||
it_id('be2fbe16-8178-485e-9a12-6fb541096480')(it)('should delete request entry after TTL', async () => {
|
||||
it('should delete request entry after TTL', async () => {
|
||||
let counter = 0;
|
||||
Parse.Cloud.define('myFunction', () => {
|
||||
counter++;
|
||||
@@ -123,7 +119,7 @@ describe('Idempotency', () => {
|
||||
}
|
||||
);
|
||||
|
||||
it_id('e976d0cc-a57f-45d4-9472-b9b052db6490')(it)('should enforce idempotency for cloud code jobs', async () => {
|
||||
it('should enforce idempotency for cloud code jobs', async () => {
|
||||
let counter = 0;
|
||||
Parse.Cloud.job('myJob', () => {
|
||||
counter++;
|
||||
@@ -145,7 +141,7 @@ describe('Idempotency', () => {
|
||||
expect(counter).toBe(1);
|
||||
});
|
||||
|
||||
it_id('7c84a3d4-e1b6-4a0d-99f1-af3cf1a6b3d8')(it)('should enforce idempotency for class object creation', async () => {
|
||||
it('should enforce idempotency for class object creation', async () => {
|
||||
let counter = 0;
|
||||
Parse.Cloud.afterSave('MyClass', () => {
|
||||
counter++;
|
||||
@@ -167,7 +163,7 @@ describe('Idempotency', () => {
|
||||
expect(counter).toBe(1);
|
||||
});
|
||||
|
||||
it_id('a030f2dd-5d21-46ac-b53d-9d714f35d72a')(it)('should enforce idempotency for user object creation', async () => {
|
||||
it('should enforce idempotency for user object creation', async () => {
|
||||
let counter = 0;
|
||||
Parse.Cloud.afterSave('_User', () => {
|
||||
counter++;
|
||||
@@ -193,7 +189,7 @@ describe('Idempotency', () => {
|
||||
expect(counter).toBe(1);
|
||||
});
|
||||
|
||||
it_id('064c469b-091c-4ba9-9043-be461f26a3eb')(it)('should enforce idempotency for installation object creation', async () => {
|
||||
it('should enforce idempotency for installation object creation', async () => {
|
||||
let counter = 0;
|
||||
Parse.Cloud.afterSave('_Installation', () => {
|
||||
counter++;
|
||||
@@ -219,7 +215,7 @@ describe('Idempotency', () => {
|
||||
expect(counter).toBe(1);
|
||||
});
|
||||
|
||||
it_id('f11670b6-fa9c-4f21-a268-ae4b6bbff7fd')(it)('should not interfere with calls of different request ID', async () => {
|
||||
it('should not interfere with calls of different request ID', async () => {
|
||||
let counter = 0;
|
||||
Parse.Cloud.afterSave('MyClass', () => {
|
||||
counter++;
|
||||
@@ -240,7 +236,7 @@ describe('Idempotency', () => {
|
||||
expect(counter).toBe(100);
|
||||
});
|
||||
|
||||
it_id('0ecd2cd2-dafb-4a2b-bb2b-9ad4c9aca777')(it)('should re-throw any other error unchanged when writing request entry fails for any other reason', async () => {
|
||||
it('should re-throw any other error unchanged when writing request entry fails for any other reason', async () => {
|
||||
spyOn(rest, 'create').and.rejectWith(new Parse.Error(0, 'some other error'));
|
||||
Parse.Cloud.define('myFunction', () => {});
|
||||
const params = {
|
||||
|
||||
@@ -4,178 +4,6 @@ const fs = require('fs');
|
||||
const port = 12345;
|
||||
const sslport = 12346;
|
||||
|
||||
describe('LDAP Injection Prevention', () => {
|
||||
describe('escapeDN', () => {
|
||||
it('should escape comma', () => {
|
||||
expect(ldap.escapeDN('admin,ou=evil')).toBe('admin\\,ou\\=evil');
|
||||
});
|
||||
|
||||
it('should escape equals sign', () => {
|
||||
expect(ldap.escapeDN('admin=evil')).toBe('admin\\=evil');
|
||||
});
|
||||
|
||||
it('should escape plus sign', () => {
|
||||
expect(ldap.escapeDN('admin+evil')).toBe('admin\\+evil');
|
||||
});
|
||||
|
||||
it('should escape less-than and greater-than signs', () => {
|
||||
expect(ldap.escapeDN('admin<evil>')).toBe('admin\\<evil\\>');
|
||||
});
|
||||
|
||||
it('should escape hash at start', () => {
|
||||
expect(ldap.escapeDN('#admin')).toBe('\\#admin');
|
||||
});
|
||||
|
||||
it('should escape semicolon', () => {
|
||||
expect(ldap.escapeDN('admin;evil')).toBe('admin\\;evil');
|
||||
});
|
||||
|
||||
it('should escape double quote', () => {
|
||||
expect(ldap.escapeDN('admin"evil')).toBe('admin\\"evil');
|
||||
});
|
||||
|
||||
it('should escape backslash', () => {
|
||||
expect(ldap.escapeDN('admin\\evil')).toBe('admin\\\\evil');
|
||||
});
|
||||
|
||||
it('should escape leading space', () => {
|
||||
expect(ldap.escapeDN(' admin')).toBe('\\ admin');
|
||||
});
|
||||
|
||||
it('should escape trailing space', () => {
|
||||
expect(ldap.escapeDN('admin ')).toBe('admin\\ ');
|
||||
});
|
||||
|
||||
it('should escape multiple special characters', () => {
|
||||
expect(ldap.escapeDN('admin,ou=evil+cn=x')).toBe('admin\\,ou\\=evil\\+cn\\=x');
|
||||
});
|
||||
|
||||
it('should not modify safe values', () => {
|
||||
expect(ldap.escapeDN('testuser')).toBe('testuser');
|
||||
expect(ldap.escapeDN('john.doe')).toBe('john.doe');
|
||||
expect(ldap.escapeDN('user123')).toBe('user123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('escapeFilter', () => {
|
||||
it('should escape asterisk', () => {
|
||||
expect(ldap.escapeFilter('*')).toBe('\\2a');
|
||||
});
|
||||
|
||||
it('should escape open parenthesis', () => {
|
||||
expect(ldap.escapeFilter('test(')).toBe('test\\28');
|
||||
});
|
||||
|
||||
it('should escape close parenthesis', () => {
|
||||
expect(ldap.escapeFilter('test)')).toBe('test\\29');
|
||||
});
|
||||
|
||||
it('should escape backslash', () => {
|
||||
expect(ldap.escapeFilter('test\\')).toBe('test\\5c');
|
||||
});
|
||||
|
||||
it('should escape null byte', () => {
|
||||
expect(ldap.escapeFilter('test\x00')).toBe('test\\00');
|
||||
});
|
||||
|
||||
it('should escape multiple special characters', () => {
|
||||
expect(ldap.escapeFilter('*()\\')).toBe('\\2a\\28\\29\\5c');
|
||||
});
|
||||
|
||||
it('should not modify safe values', () => {
|
||||
expect(ldap.escapeFilter('testuser')).toBe('testuser');
|
||||
expect(ldap.escapeFilter('john.doe')).toBe('john.doe');
|
||||
expect(ldap.escapeFilter('user123')).toBe('user123');
|
||||
});
|
||||
|
||||
it('should escape filter injection attempt with wildcard', () => {
|
||||
expect(ldap.escapeFilter('x)(|(objectClass=*)')).toBe('x\\29\\28|\\28objectClass=\\2a\\29');
|
||||
});
|
||||
});
|
||||
|
||||
describe('authData validation', () => {
|
||||
it('should reject missing authData.id', async done => {
|
||||
const server = await mockLdapServer(port, 'uid=testuser, o=example');
|
||||
const options = {
|
||||
suffix: 'o=example',
|
||||
url: `ldap://localhost:${port}`,
|
||||
dn: 'uid={{id}}, o=example',
|
||||
};
|
||||
try {
|
||||
await ldap.validateAuthData({ password: 'secret' }, options);
|
||||
fail('Should have rejected missing id');
|
||||
} catch (err) {
|
||||
expect(err.message).toBe('LDAP: Wrong username or password');
|
||||
}
|
||||
server.close(done);
|
||||
});
|
||||
|
||||
it('should reject non-string authData.id', async done => {
|
||||
const server = await mockLdapServer(port, 'uid=testuser, o=example');
|
||||
const options = {
|
||||
suffix: 'o=example',
|
||||
url: `ldap://localhost:${port}`,
|
||||
dn: 'uid={{id}}, o=example',
|
||||
};
|
||||
try {
|
||||
await ldap.validateAuthData({ id: 123, password: 'secret' }, options);
|
||||
fail('Should have rejected non-string id');
|
||||
} catch (err) {
|
||||
expect(err.message).toBe('LDAP: Wrong username or password');
|
||||
}
|
||||
server.close(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DN injection prevention', () => {
|
||||
it('should prevent DN injection via comma in authData.id', async done => {
|
||||
// Mock server accepts the DN that would result from an unescaped injection
|
||||
const server = await mockLdapServer(port, 'uid=admin,ou=admins,o=example');
|
||||
const options = {
|
||||
suffix: 'o=example',
|
||||
url: `ldap://localhost:${port}`,
|
||||
dn: 'uid={{id}}, o=example',
|
||||
};
|
||||
// Attacker tries to inject additional DN components via comma
|
||||
// Without escaping: DN = uid=admin,ou=admins, o=example (3 RDNs) → matches mock
|
||||
// With escaping: DN = uid=admin\,ou=admins, o=example (2 RDNs) → doesn't match
|
||||
try {
|
||||
await ldap.validateAuthData({ id: 'admin,ou=admins', password: 'secret' }, options);
|
||||
fail('Should have rejected DN injection attempt');
|
||||
} catch (err) {
|
||||
expect(err.message).toBe('LDAP: Wrong username or password');
|
||||
}
|
||||
server.close(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Filter injection prevention', () => {
|
||||
it('should prevent LDAP filter injection via wildcard in authData.id', async done => {
|
||||
// Mock server accepts uid=*, o=example (the attacker's bind DN)
|
||||
// The * is not special in DNs so it binds fine regardless of escaping
|
||||
const server = await mockLdapServer(port, 'uid=*, o=example');
|
||||
const options = {
|
||||
suffix: 'o=example',
|
||||
url: `ldap://localhost:${port}`,
|
||||
dn: 'uid={{id}}, o=example',
|
||||
groupCn: 'powerusers',
|
||||
groupFilter: '(&(uniqueMember=uid={{id}}, o=example)(objectClass=groupOfUniqueNames))',
|
||||
};
|
||||
// Attacker uses * as ID to match any group member via wildcard
|
||||
// Group has member uid=testuser, not uid=*
|
||||
// Without escaping: filter uses SubstringFilter, matches testuser → passes
|
||||
// With escaping: filter uses EqualityFilter with literal \2a, no match → fails
|
||||
try {
|
||||
await ldap.validateAuthData({ id: '*', password: 'secret' }, options);
|
||||
fail('Should have rejected filter injection attempt');
|
||||
} catch (err) {
|
||||
expect(err.message).toBe('LDAP: User not in group');
|
||||
}
|
||||
server.close(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Ldap Auth', () => {
|
||||
it('Should fail with missing options', done => {
|
||||
ldap
|
||||
|
||||
@@ -52,9 +52,6 @@ describe_only(() => {
|
||||
});
|
||||
|
||||
it('can check invalid master key of request', done => {
|
||||
const logger = require('../lib/logger').default;
|
||||
const loggerErrorSpy = spyOn(logger, 'error').and.callThrough();
|
||||
loggerErrorSpy.calls.reset();
|
||||
request({
|
||||
url: 'http://localhost:8378/1/scriptlog',
|
||||
headers: {
|
||||
@@ -64,8 +61,7 @@ describe_only(() => {
|
||||
}).then(fail, response => {
|
||||
const body = response.data;
|
||||
expect(response.status).toEqual(403);
|
||||
expect(body.error).toEqual('Permission denied');
|
||||
expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required'));
|
||||
expect(body.error).toEqual('unauthorized: master key is required');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -79,7 +75,7 @@ describe_only(() => {
|
||||
/**
|
||||
* Verifies simple passwords in GET login requests with special characters are scrubbed from the verbose log
|
||||
*/
|
||||
it_id('e36d6141-2a20-41d0-85fc-d1534c3e4bae')(it)('does scrub simple passwords on GET login', done => {
|
||||
it('does scrub simple passwords on GET login', done => {
|
||||
reconfigureServer({
|
||||
verbose: true,
|
||||
}).then(function () {
|
||||
@@ -109,7 +105,7 @@ describe_only(() => {
|
||||
/**
|
||||
* Verifies complex passwords in GET login requests with special characters are scrubbed from the verbose log
|
||||
*/
|
||||
it_id('24b277c5-250f-4a35-a449-2c8c519d4c03')(it)('does scrub complex passwords on GET login', done => {
|
||||
it('does scrub complex passwords on GET login', done => {
|
||||
reconfigureServer({
|
||||
verbose: true,
|
||||
})
|
||||
@@ -143,7 +139,7 @@ describe_only(() => {
|
||||
/**
|
||||
* Verifies fields in POST login requests are NOT present in the verbose log
|
||||
*/
|
||||
it_id('33143ec9-b32d-467c-ba65-ff2bbefdaadd')(it)('does not have password field in POST login', done => {
|
||||
it('does not have password field in POST login', done => {
|
||||
reconfigureServer({
|
||||
verbose: true,
|
||||
}).then(function () {
|
||||
|
||||
+27
-25
@@ -32,10 +32,7 @@ describe('middlewares', () => {
|
||||
AppCache.del(fakeReq.body._ApplicationId);
|
||||
});
|
||||
|
||||
it_id('4cc18d90-1763-4725-97fa-f63fb4692fc4')(it)('should use _ContentType if provided', done => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKeyIps: ['127.0.0.1'],
|
||||
});
|
||||
it('should use _ContentType if provided', done => {
|
||||
expect(fakeReq.headers['content-type']).toEqual(undefined);
|
||||
const contentType = 'image/jpeg';
|
||||
fakeReq.body._ContentType = contentType;
|
||||
@@ -46,32 +43,32 @@ describe('middlewares', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should give invalid response when keys are configured but no key supplied', async () => {
|
||||
it('should give invalid response when keys are configured but no key supplied', () => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
restAPIKey: 'restAPIKey',
|
||||
});
|
||||
await middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
expect(fakeRes.status).toHaveBeenCalledWith(403);
|
||||
});
|
||||
|
||||
it('should give invalid response when keys are configured but supplied key is incorrect', async () => {
|
||||
it('should give invalid response when keys are configured but supplied key is incorrect', () => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
restAPIKey: 'restAPIKey',
|
||||
});
|
||||
fakeReq.headers['x-parse-rest-api-key'] = 'wrongKey';
|
||||
await middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
expect(fakeRes.status).toHaveBeenCalledWith(403);
|
||||
});
|
||||
|
||||
it('should give invalid response when keys are configured but different key is supplied', async () => {
|
||||
it('should give invalid response when keys are configured but different key is supplied', () => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
restAPIKey: 'restAPIKey',
|
||||
});
|
||||
fakeReq.headers['x-parse-client-key'] = 'clientKey';
|
||||
await middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
expect(fakeRes.status).toHaveBeenCalledWith(403);
|
||||
});
|
||||
|
||||
@@ -128,7 +125,7 @@ describe('middlewares', () => {
|
||||
const otherKeys = BodyKeys.filter(
|
||||
otherKey => otherKey !== infoKey && otherKey !== 'javascriptKey'
|
||||
);
|
||||
it_id('f9abd7ac-b1f4-4607-b9b0-365ff0559d84')(it)(`it should pull ${bodyKey} into req.info`, done => {
|
||||
it(`it should pull ${bodyKey} into req.info`, done => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKeyIps: ['0.0.0.0/0'],
|
||||
});
|
||||
@@ -147,7 +144,7 @@ describe('middlewares', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_id('4a0bce41-c536-4482-a873-12ed023380e2')(it)('should not succeed and log if the ip does not belong to masterKeyIps list', async () => {
|
||||
it('should not succeed and log if the ip does not belong to masterKeyIps list', async () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
spyOn(logger, 'error').and.callFake(() => {});
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
@@ -156,17 +153,25 @@ describe('middlewares', () => {
|
||||
});
|
||||
fakeReq.ip = '127.0.0.1';
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
|
||||
const error = await middlewares.handleParseHeaders(fakeReq, fakeRes, () => {}).catch(e => e);
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toEqual(`unauthorized`);
|
||||
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
|
||||
expect(fakeReq.auth.isMaster).toBe(false);
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
`Request using master key rejected as the request IP address '127.0.0.1' is not set in Parse Server option 'masterKeyIps'.`
|
||||
);
|
||||
});
|
||||
|
||||
it('should not succeed and log if the ip does not belong to maintenanceKeyIps list', async () => {
|
||||
it('should not succeed if the ip does not belong to masterKeyIps list', async () => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['10.0.0.1'],
|
||||
});
|
||||
fakeReq.ip = '127.0.0.1';
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
|
||||
expect(fakeReq.auth.isMaster).toBe(false);
|
||||
});
|
||||
|
||||
it('should not succeed if the ip does not belong to maintenanceKeyIps list', async () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
spyOn(logger, 'error').and.callFake(() => {});
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
@@ -175,17 +180,14 @@ describe('middlewares', () => {
|
||||
});
|
||||
fakeReq.ip = '10.0.0.2';
|
||||
fakeReq.headers['x-parse-maintenance-key'] = 'masterKey';
|
||||
|
||||
const error = await middlewares.handleParseHeaders(fakeReq, fakeRes, () => {}).catch(e => e);
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toEqual(`unauthorized`);
|
||||
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
|
||||
expect(fakeReq.auth.isMaintenance).toBe(false);
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
`Request using maintenance key rejected as the request IP address '10.0.0.2' is not set in Parse Server option 'maintenanceKeyIps'.`
|
||||
);
|
||||
});
|
||||
|
||||
it_id('2f7fadec-a87c-4626-90d1-65c75653aea9')(it)('should succeed if the ip does belong to masterKeyIps list', async () => {
|
||||
it('should succeed if the ip does belong to masterKeyIps list', async () => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['10.0.0.1'],
|
||||
@@ -196,7 +198,7 @@ describe('middlewares', () => {
|
||||
expect(fakeReq.auth.isMaster).toBe(true);
|
||||
});
|
||||
|
||||
it_id('2b251fd4-d43c-48f4-ada9-c8458e40c12a')(it)('should allow any ip to use masterKey if masterKeyIps is empty', async () => {
|
||||
it('should allow any ip to use masterKey if masterKeyIps is empty', async () => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['0.0.0.0/0'],
|
||||
|
||||
@@ -18,12 +18,6 @@ describe('MongoSchemaCollection', () => {
|
||||
},
|
||||
_metadata: {
|
||||
class_permissions: {
|
||||
ACL: {
|
||||
'*': {
|
||||
read: true,
|
||||
write: true,
|
||||
},
|
||||
},
|
||||
get: { '*': true },
|
||||
find: { '*': true },
|
||||
count: { '*': true },
|
||||
@@ -75,12 +69,6 @@ describe('MongoSchemaCollection', () => {
|
||||
objectId: { type: 'String' },
|
||||
},
|
||||
classLevelPermissions: {
|
||||
ACL: {
|
||||
'*': {
|
||||
read: true,
|
||||
write: true,
|
||||
},
|
||||
},
|
||||
find: { '*': true },
|
||||
get: { '*': true },
|
||||
count: { '*': true },
|
||||
|
||||
@@ -15,8 +15,8 @@ const fakeClient = {
|
||||
// These tests are specific to the mongo storage adapter + mongo storage format
|
||||
// and will eventually be moved into their own repo
|
||||
describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
beforeEach(async () => {
|
||||
await new MongoStorageAdapter({ uri: databaseURI }).deleteAllClasses();
|
||||
beforeEach(done => {
|
||||
new MongoStorageAdapter({ uri: databaseURI }).deleteAllClasses().then(done, fail);
|
||||
Config.get(Parse.applicationId).schemaCache.clear();
|
||||
});
|
||||
|
||||
@@ -284,11 +284,25 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
amount: 1,
|
||||
},
|
||||
};
|
||||
await Parse.Server.database.update('MyClass', query, update, { upsert: true });
|
||||
await Parse.Server.database.update(
|
||||
'MyClass',
|
||||
query,
|
||||
update,
|
||||
{ upsert: true },
|
||||
);
|
||||
update.objectId.amount = uuid2;
|
||||
await Parse.Server.database.update('MyClass', query, update, { upsert: true });
|
||||
await Parse.Server.database.update(
|
||||
'MyClass',
|
||||
query,
|
||||
update,
|
||||
{ upsert: true },
|
||||
);
|
||||
|
||||
const res = await Parse.Server.database.find(schema.className, {}, {});
|
||||
const res = await Parse.Server.database.find(
|
||||
schema.className,
|
||||
{},
|
||||
{},
|
||||
);
|
||||
expect(res.length).toBe(1);
|
||||
expect(res[0].objectId).toBe(uuid1);
|
||||
expect(res[0].count).toBe(2);
|
||||
@@ -390,7 +404,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
await expectAsync(adapter.getClass('UnknownClass')).toBeRejectedWith(undefined);
|
||||
});
|
||||
|
||||
it_only_mongodb_version('<5.1 || >=6')('should use index for caseInsensitive query', async () => {
|
||||
it_only_mongodb_version('<5.1>=6')('should use index for caseInsensitive query', async () => {
|
||||
const user = new Parse.User();
|
||||
user.set('username', 'Bugs');
|
||||
user.set('password', 'Bunny');
|
||||
@@ -424,6 +438,40 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
expect(postIndexPlan.executionStats.executionStages.stage).toBe('FETCH');
|
||||
});
|
||||
|
||||
it_only_mongodb_version('>=5.1<6')('should use index for caseInsensitive query', async () => {
|
||||
const user = new Parse.User();
|
||||
user.set('username', 'Bugs');
|
||||
user.set('password', 'Bunny');
|
||||
await user.signUp();
|
||||
|
||||
const database = Config.get(Parse.applicationId).database;
|
||||
await database.adapter.dropAllIndexes('_User');
|
||||
|
||||
const preIndexPlan = await database.find(
|
||||
'_User',
|
||||
{ username: 'bugs' },
|
||||
{ caseInsensitive: true, explain: true }
|
||||
);
|
||||
|
||||
const schema = await new Parse.Schema('_User').get();
|
||||
|
||||
await database.adapter.ensureIndex(
|
||||
'_User',
|
||||
schema,
|
||||
['username'],
|
||||
'case_insensitive_username',
|
||||
true
|
||||
);
|
||||
|
||||
const postIndexPlan = await database.find(
|
||||
'_User',
|
||||
{ username: 'bugs' },
|
||||
{ caseInsensitive: true, explain: true }
|
||||
);
|
||||
expect(preIndexPlan.queryPlanner.winningPlan.queryPlan.stage).toBe('COLLSCAN');
|
||||
expect(postIndexPlan.queryPlanner.winningPlan.queryPlan.stage).toBe('FETCH');
|
||||
});
|
||||
|
||||
it('should delete field without index', async () => {
|
||||
const database = Config.get(Parse.applicationId).database;
|
||||
const obj = new Parse.Object('MyObject');
|
||||
@@ -649,418 +697,4 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('index creation options', () => {
|
||||
beforeEach(async () => {
|
||||
await new MongoStorageAdapter({ uri: databaseURI }).deleteAllClasses();
|
||||
});
|
||||
|
||||
async function getIndexes(collectionName) {
|
||||
const adapter = Config.get(Parse.applicationId).database.adapter;
|
||||
const collections = await adapter.database.listCollections({ name: collectionName }).toArray();
|
||||
if (collections.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return await adapter.database.collection(collectionName).indexes();
|
||||
}
|
||||
|
||||
it('should skip username index when createIndexUserUsername is false', async () => {
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI,
|
||||
databaseOptions: { createIndexUserUsername: false },
|
||||
});
|
||||
const indexes = await getIndexes('_User');
|
||||
expect(indexes.find(idx => idx.name === 'username_1')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should create username index when createIndexUserUsername is true', async () => {
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI,
|
||||
databaseOptions: { createIndexUserUsername: true },
|
||||
});
|
||||
const indexes = await getIndexes('_User');
|
||||
expect(indexes.find(idx => idx.name === 'username_1')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should skip case-insensitive username index when createIndexUserUsernameCaseInsensitive is false', async () => {
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI,
|
||||
databaseOptions: { createIndexUserUsernameCaseInsensitive: false },
|
||||
});
|
||||
const indexes = await getIndexes('_User');
|
||||
expect(indexes.find(idx => idx.name === 'case_insensitive_username')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should create case-insensitive username index when createIndexUserUsernameCaseInsensitive is true', async () => {
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI,
|
||||
databaseOptions: { createIndexUserUsernameCaseInsensitive: true },
|
||||
});
|
||||
const indexes = await getIndexes('_User');
|
||||
expect(indexes.find(idx => idx.name === 'case_insensitive_username')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should skip email index when createIndexUserEmail is false', async () => {
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI,
|
||||
databaseOptions: { createIndexUserEmail: false },
|
||||
});
|
||||
const indexes = await getIndexes('_User');
|
||||
expect(indexes.find(idx => idx.name === 'email_1')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should create email index when createIndexUserEmail is true', async () => {
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI,
|
||||
databaseOptions: { createIndexUserEmail: true },
|
||||
});
|
||||
const indexes = await getIndexes('_User');
|
||||
expect(indexes.find(idx => idx.name === 'email_1')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should skip case-insensitive email index when createIndexUserEmailCaseInsensitive is false', async () => {
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI,
|
||||
databaseOptions: { createIndexUserEmailCaseInsensitive: false },
|
||||
});
|
||||
const indexes = await getIndexes('_User');
|
||||
expect(indexes.find(idx => idx.name === 'case_insensitive_email')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should create case-insensitive email index when createIndexUserEmailCaseInsensitive is true', async () => {
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI,
|
||||
databaseOptions: { createIndexUserEmailCaseInsensitive: true },
|
||||
});
|
||||
const indexes = await getIndexes('_User');
|
||||
expect(indexes.find(idx => idx.name === 'case_insensitive_email')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should skip email verify token index when createIndexUserEmailVerifyToken is false', async () => {
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI,
|
||||
databaseOptions: { createIndexUserEmailVerifyToken: false },
|
||||
});
|
||||
const indexes = await getIndexes('_User');
|
||||
expect(indexes.find(idx => idx.name === '_email_verify_token' || idx.name === '_email_verify_token_1')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should create email verify token index when createIndexUserEmailVerifyToken is true', async () => {
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI,
|
||||
databaseOptions: { createIndexUserEmailVerifyToken: true },
|
||||
});
|
||||
const indexes = await getIndexes('_User');
|
||||
expect(indexes.find(idx => idx.name === '_email_verify_token' || idx.name === '_email_verify_token_1')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should skip password reset token index when createIndexUserPasswordResetToken is false', async () => {
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI,
|
||||
databaseOptions: { createIndexUserPasswordResetToken: false },
|
||||
});
|
||||
const indexes = await getIndexes('_User');
|
||||
expect(indexes.find(idx => idx.name === '_perishable_token' || idx.name === '_perishable_token_1')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should create password reset token index when createIndexUserPasswordResetToken is true', async () => {
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI,
|
||||
databaseOptions: { createIndexUserPasswordResetToken: true },
|
||||
});
|
||||
const indexes = await getIndexes('_User');
|
||||
expect(indexes.find(idx => idx.name === '_perishable_token' || idx.name === '_perishable_token_1')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should skip role name index when createIndexRoleName is false', async () => {
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI,
|
||||
databaseOptions: { createIndexRoleName: false },
|
||||
});
|
||||
const indexes = await getIndexes('_Role');
|
||||
expect(indexes.find(idx => idx.name === 'name_1')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should create role name index when createIndexRoleName is true', async () => {
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI,
|
||||
databaseOptions: { createIndexRoleName: true },
|
||||
});
|
||||
const indexes = await getIndexes('_Role');
|
||||
expect(indexes.find(idx => idx.name === 'name_1')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should create all indexes by default when options are undefined', async () => {
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI,
|
||||
databaseOptions: {},
|
||||
});
|
||||
|
||||
const userIndexes = await getIndexes('_User');
|
||||
const roleIndexes = await getIndexes('_Role');
|
||||
|
||||
// Verify all indexes are created with default behavior (backward compatibility)
|
||||
expect(userIndexes.find(idx => idx.name === 'username_1')).toBeDefined();
|
||||
expect(userIndexes.find(idx => idx.name === 'case_insensitive_username')).toBeDefined();
|
||||
expect(userIndexes.find(idx => idx.name === 'email_1')).toBeDefined();
|
||||
expect(userIndexes.find(idx => idx.name === 'case_insensitive_email')).toBeDefined();
|
||||
expect(userIndexes.find(idx => idx.name === '_email_verify_token' || idx.name === '_email_verify_token_1')).toBeDefined();
|
||||
expect(userIndexes.find(idx => idx.name === '_perishable_token' || idx.name === '_perishable_token_1')).toBeDefined();
|
||||
expect(roleIndexes.find(idx => idx.name === 'name_1')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('logClientEvents', () => {
|
||||
it('should log MongoDB client events when configured', async () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
const logSpy = spyOn(logger, 'warn');
|
||||
|
||||
const logClientEvents = [
|
||||
{
|
||||
name: 'serverDescriptionChanged',
|
||||
keys: ['address'],
|
||||
logLevel: 'warn',
|
||||
},
|
||||
];
|
||||
|
||||
const adapter = new MongoStorageAdapter({
|
||||
uri: databaseURI,
|
||||
mongoOptions: { logClientEvents },
|
||||
});
|
||||
|
||||
// Connect to trigger event listeners setup
|
||||
await adapter.connect();
|
||||
|
||||
// Manually trigger the event to test the listener
|
||||
const mockEvent = {
|
||||
address: 'localhost:27017',
|
||||
previousDescription: { type: 'Unknown' },
|
||||
newDescription: { type: 'Standalone' },
|
||||
};
|
||||
|
||||
adapter.client.emit('serverDescriptionChanged', mockEvent);
|
||||
|
||||
// Verify the log was called with the correct message
|
||||
expect(logSpy).toHaveBeenCalledWith(
|
||||
jasmine.stringMatching(/MongoDB client event serverDescriptionChanged:.*"address":"localhost:27017"/)
|
||||
);
|
||||
|
||||
await adapter.handleShutdown();
|
||||
});
|
||||
|
||||
it('should log entire event when keys are not specified', async () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
const logSpy = spyOn(logger, 'info');
|
||||
|
||||
const logClientEvents = [
|
||||
{
|
||||
name: 'connectionPoolReady',
|
||||
logLevel: 'info',
|
||||
},
|
||||
];
|
||||
|
||||
const adapter = new MongoStorageAdapter({
|
||||
uri: databaseURI,
|
||||
mongoOptions: { logClientEvents },
|
||||
});
|
||||
|
||||
await adapter.connect();
|
||||
|
||||
const mockEvent = {
|
||||
address: 'localhost:27017',
|
||||
options: { maxPoolSize: 100 },
|
||||
};
|
||||
|
||||
adapter.client.emit('connectionPoolReady', mockEvent);
|
||||
|
||||
expect(logSpy).toHaveBeenCalledWith(
|
||||
jasmine.stringMatching(/MongoDB client event connectionPoolReady:.*"address":"localhost:27017".*"options"/)
|
||||
);
|
||||
|
||||
await adapter.handleShutdown();
|
||||
});
|
||||
|
||||
it('should extract nested keys using dot notation', async () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
const logSpy = spyOn(logger, 'warn');
|
||||
|
||||
const logClientEvents = [
|
||||
{
|
||||
name: 'topologyDescriptionChanged',
|
||||
keys: ['previousDescription.type', 'newDescription.type', 'newDescription.servers.size'],
|
||||
logLevel: 'warn',
|
||||
},
|
||||
];
|
||||
|
||||
const adapter = new MongoStorageAdapter({
|
||||
uri: databaseURI,
|
||||
mongoOptions: { logClientEvents },
|
||||
});
|
||||
|
||||
await adapter.connect();
|
||||
|
||||
const mockEvent = {
|
||||
topologyId: 1,
|
||||
previousDescription: { type: 'Unknown' },
|
||||
newDescription: {
|
||||
type: 'ReplicaSetWithPrimary',
|
||||
servers: { size: 3 },
|
||||
},
|
||||
};
|
||||
|
||||
adapter.client.emit('topologyDescriptionChanged', mockEvent);
|
||||
|
||||
expect(logSpy).toHaveBeenCalledWith(
|
||||
jasmine.stringMatching(/MongoDB client event topologyDescriptionChanged:.*"previousDescription.type":"Unknown".*"newDescription.type":"ReplicaSetWithPrimary".*"newDescription.servers.size":3/)
|
||||
);
|
||||
|
||||
await adapter.handleShutdown();
|
||||
});
|
||||
|
||||
it('should handle invalid log level gracefully', async () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
const infoSpy = spyOn(logger, 'info');
|
||||
|
||||
const logClientEvents = [
|
||||
{
|
||||
name: 'connectionPoolReady',
|
||||
keys: ['address'],
|
||||
logLevel: 'invalidLogLevel', // Invalid log level
|
||||
},
|
||||
];
|
||||
|
||||
const adapter = new MongoStorageAdapter({
|
||||
uri: databaseURI,
|
||||
mongoOptions: { logClientEvents },
|
||||
});
|
||||
|
||||
await adapter.connect();
|
||||
|
||||
const mockEvent = {
|
||||
address: 'localhost:27017',
|
||||
};
|
||||
|
||||
adapter.client.emit('connectionPoolReady', mockEvent);
|
||||
|
||||
// Should fallback to 'info' level
|
||||
expect(infoSpy).toHaveBeenCalledWith(
|
||||
jasmine.stringMatching(/MongoDB client event connectionPoolReady:.*"address":"localhost:27017"/)
|
||||
);
|
||||
|
||||
await adapter.handleShutdown();
|
||||
});
|
||||
|
||||
it('should handle Map and Set instances in events', async () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
const warnSpy = spyOn(logger, 'warn');
|
||||
|
||||
const logClientEvents = [
|
||||
{
|
||||
name: 'customEvent',
|
||||
logLevel: 'warn',
|
||||
},
|
||||
];
|
||||
|
||||
const adapter = new MongoStorageAdapter({
|
||||
uri: databaseURI,
|
||||
mongoOptions: { logClientEvents },
|
||||
});
|
||||
|
||||
await adapter.connect();
|
||||
|
||||
const mockEvent = {
|
||||
mapData: new Map([['key1', 'value1'], ['key2', 'value2']]),
|
||||
setData: new Set([1, 2, 3]),
|
||||
};
|
||||
|
||||
adapter.client.emit('customEvent', mockEvent);
|
||||
|
||||
// Should serialize Map and Set properly
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
jasmine.stringMatching(/MongoDB client event customEvent:.*"mapData":\{"key1":"value1","key2":"value2"\}.*"setData":\[1,2,3\]/)
|
||||
);
|
||||
|
||||
await adapter.handleShutdown();
|
||||
});
|
||||
|
||||
it('should handle missing keys in event object', async () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
const infoSpy = spyOn(logger, 'info');
|
||||
|
||||
const logClientEvents = [
|
||||
{
|
||||
name: 'testEvent',
|
||||
keys: ['nonexistent.nested.key', 'another.missing'],
|
||||
logLevel: 'info',
|
||||
},
|
||||
];
|
||||
|
||||
const adapter = new MongoStorageAdapter({
|
||||
uri: databaseURI,
|
||||
mongoOptions: { logClientEvents },
|
||||
});
|
||||
|
||||
await adapter.connect();
|
||||
|
||||
const mockEvent = {
|
||||
actualField: 'value',
|
||||
};
|
||||
|
||||
adapter.client.emit('testEvent', mockEvent);
|
||||
|
||||
// Should handle missing keys gracefully with undefined values
|
||||
expect(infoSpy).toHaveBeenCalledWith(
|
||||
jasmine.stringMatching(/MongoDB client event testEvent:/)
|
||||
);
|
||||
|
||||
await adapter.handleShutdown();
|
||||
});
|
||||
|
||||
it('should handle circular references gracefully', async () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
const infoSpy = spyOn(logger, 'info');
|
||||
|
||||
const logClientEvents = [
|
||||
{
|
||||
name: 'circularEvent',
|
||||
logLevel: 'info',
|
||||
},
|
||||
];
|
||||
|
||||
const adapter = new MongoStorageAdapter({
|
||||
uri: databaseURI,
|
||||
mongoOptions: { logClientEvents },
|
||||
});
|
||||
|
||||
await adapter.connect();
|
||||
|
||||
// Create circular reference
|
||||
const mockEvent = { name: 'test' };
|
||||
mockEvent.self = mockEvent;
|
||||
|
||||
adapter.client.emit('circularEvent', mockEvent);
|
||||
|
||||
// Should handle circular reference with [Circular] marker
|
||||
expect(infoSpy).toHaveBeenCalledWith(
|
||||
jasmine.stringMatching(/MongoDB client event circularEvent:.*\[Circular\]/)
|
||||
);
|
||||
|
||||
await adapter.handleShutdown();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -521,23 +521,6 @@ describe('parseObjectToMongoObjectForCreate', () => {
|
||||
expect(output.authData).toBe('random');
|
||||
done();
|
||||
});
|
||||
|
||||
it('should only transform authData.provider.id for _User class', () => {
|
||||
// Test that for _User class, authData.facebook.id is transformed
|
||||
const userInput = {
|
||||
'authData.facebook.id': '10000000000000001',
|
||||
};
|
||||
const userOutput = transform.transformWhere('_User', userInput, { fields: {} });
|
||||
expect(userOutput['_auth_data_facebook.id']).toBe('10000000000000001');
|
||||
|
||||
// Test that for non-User classes, authData.facebook.id is NOT transformed
|
||||
const customInput = {
|
||||
'authData.facebook.id': '10000000000000001',
|
||||
};
|
||||
const customOutput = transform.transformWhere('SpamAlerts', customInput, { fields: {} });
|
||||
expect(customOutput['authData.facebook.id']).toBe('10000000000000001');
|
||||
expect(customOutput['_auth_data_facebook.id']).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot have a custom field name beginning with underscore', done => {
|
||||
@@ -692,5 +675,4 @@ describe('relativeTimeToDate', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
+43
-271
@@ -1,7 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
const request = require('../lib/request');
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
const mustache = require('mustache');
|
||||
const Utils = require('../lib/Utils');
|
||||
@@ -109,7 +108,7 @@ describe('Pages Router', () => {
|
||||
const res = await request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: `new_password=user1&token=43634643`,
|
||||
body: `new_password=user1&token=43634643&username=username`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
@@ -125,7 +124,7 @@ describe('Pages Router', () => {
|
||||
await request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: `new_password=&token=132414`,
|
||||
body: `new_password=&token=132414&username=Johnny`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
@@ -138,12 +137,30 @@ describe('Pages Router', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('request_password_reset: responds with AJAX error on missing username', async () => {
|
||||
try {
|
||||
await request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: `new_password=user1&token=43634643&username=`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
followRedirects: false,
|
||||
});
|
||||
} catch (error) {
|
||||
expect(error.status).not.toBe(302);
|
||||
expect(error.text).toEqual('{"code":200,"error":"Missing username"}');
|
||||
}
|
||||
});
|
||||
|
||||
it('request_password_reset: responds with AJAX error on missing token', async () => {
|
||||
try {
|
||||
await request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: `new_password=user1&token=`,
|
||||
body: `new_password=user1&token=&username=Johnny`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
@@ -560,7 +577,7 @@ describe('Pages Router', () => {
|
||||
spyOnProperty(Page.prototype, 'defaultFile').and.returnValue(jsonPageFile);
|
||||
|
||||
const response = await request({
|
||||
url: `http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&locale=${exampleLocale}`,
|
||||
url: `http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&username=exampleUsername&locale=${exampleLocale}`,
|
||||
followRedirects: false,
|
||||
}).catch(e => e);
|
||||
expect(response.status).toEqual(200);
|
||||
@@ -609,7 +626,7 @@ describe('Pages Router', () => {
|
||||
await reconfigureServer(config);
|
||||
const response = await request({
|
||||
url:
|
||||
'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&locale=de-AT',
|
||||
'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&username=exampleUsername&locale=de-AT',
|
||||
followRedirects: false,
|
||||
method: 'POST',
|
||||
});
|
||||
@@ -623,7 +640,7 @@ describe('Pages Router', () => {
|
||||
await reconfigureServer(config);
|
||||
const response = await request({
|
||||
url:
|
||||
'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&locale=de-AT',
|
||||
'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&username=exampleUsername&locale=de-AT',
|
||||
followRedirects: false,
|
||||
method: 'GET',
|
||||
});
|
||||
@@ -659,11 +676,13 @@ describe('Pages Router', () => {
|
||||
const appId = linkResponse.headers['x-parse-page-param-appid'];
|
||||
const token = linkResponse.headers['x-parse-page-param-token'];
|
||||
const locale = linkResponse.headers['x-parse-page-param-locale'];
|
||||
const username = linkResponse.headers['x-parse-page-param-username'];
|
||||
const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl'];
|
||||
const passwordResetPagePath = pageResponse.calls.all()[0].args[0];
|
||||
expect(appId).toBeDefined();
|
||||
expect(token).toBeDefined();
|
||||
expect(locale).toBeDefined();
|
||||
expect(username).toBeDefined();
|
||||
expect(publicServerUrl).toBeDefined();
|
||||
expect(passwordResetPagePath).toMatch(
|
||||
new RegExp(`\/${exampleLocale}\/${pages.passwordReset.defaultFile}`)
|
||||
@@ -677,6 +696,7 @@ describe('Pages Router', () => {
|
||||
body: {
|
||||
token,
|
||||
locale,
|
||||
username,
|
||||
new_password: 'newPassword',
|
||||
},
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
@@ -718,7 +738,7 @@ describe('Pages Router', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it_id('2845c2ea-23ba-45d2-a33f-63181d419bca')(it)('localizes end-to-end for verify email: success', async () => {
|
||||
it('localizes end-to-end for verify email: success', async () => {
|
||||
await reconfigureServer(config);
|
||||
const sendVerificationEmail = spyOn(
|
||||
config.emailAdapter,
|
||||
@@ -729,7 +749,6 @@ describe('Pages Router', () => {
|
||||
user.setPassword('examplePassword');
|
||||
user.set('email', 'mail@example.com');
|
||||
await user.signUp();
|
||||
await jasmine.timeout();
|
||||
|
||||
const link = sendVerificationEmail.calls.all()[0].args[0].link;
|
||||
const linkWithLocale = new URL(link);
|
||||
@@ -747,7 +766,7 @@ describe('Pages Router', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it_id('f2272b94-b4ac-474f-8e47-1ca74de136f5')(it)('localizes end-to-end for verify email: invalid verification link - link send success', async () => {
|
||||
it('localizes end-to-end for verify email: invalid verification link - link send success', async () => {
|
||||
await reconfigureServer(config);
|
||||
const sendVerificationEmail = spyOn(
|
||||
config.emailAdapter,
|
||||
@@ -758,7 +777,6 @@ describe('Pages Router', () => {
|
||||
user.setPassword('examplePassword');
|
||||
user.set('email', 'mail@example.com');
|
||||
await user.signUp();
|
||||
await jasmine.timeout();
|
||||
|
||||
const link = sendVerificationEmail.calls.all()[0].args[0].link;
|
||||
const linkWithLocale = new URL(link);
|
||||
@@ -773,13 +791,15 @@ describe('Pages Router', () => {
|
||||
|
||||
const appId = linkResponse.headers['x-parse-page-param-appid'];
|
||||
const locale = linkResponse.headers['x-parse-page-param-locale'];
|
||||
const username = linkResponse.headers['x-parse-page-param-username'];
|
||||
const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl'];
|
||||
const invalidVerificationPagePath = pageResponse.calls.all()[0].args[0];
|
||||
expect(appId).toBeDefined();
|
||||
expect(locale).toBe(exampleLocale);
|
||||
expect(username).toBeDefined();
|
||||
expect(publicServerUrl).toBeDefined();
|
||||
expect(invalidVerificationPagePath).toMatch(
|
||||
new RegExp(`\/${exampleLocale}\/${pages.emailVerificationLinkInvalid.defaultFile}`)
|
||||
new RegExp(`\/${exampleLocale}\/${pages.emailVerificationLinkExpired.defaultFile}`)
|
||||
);
|
||||
|
||||
const formUrl = `${publicServerUrl}/apps/${appId}/resend_verification_email`;
|
||||
@@ -788,7 +808,7 @@ describe('Pages Router', () => {
|
||||
method: 'POST',
|
||||
body: {
|
||||
locale,
|
||||
username: 'exampleUsername',
|
||||
username,
|
||||
},
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
followRedirects: false,
|
||||
@@ -799,7 +819,7 @@ describe('Pages Router', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it_id('1d46d36a-e455-4ae7-8717-e0d286e95f02')(it)('localizes end-to-end for verify email: invalid verification link - link send fail', async () => {
|
||||
it('localizes end-to-end for verify email: invalid verification link - link send fail', async () => {
|
||||
await reconfigureServer(config);
|
||||
const sendVerificationEmail = spyOn(
|
||||
config.emailAdapter,
|
||||
@@ -810,7 +830,6 @@ describe('Pages Router', () => {
|
||||
user.setPassword('examplePassword');
|
||||
user.set('email', 'mail@example.com');
|
||||
await user.signUp();
|
||||
await jasmine.timeout();
|
||||
|
||||
const link = sendVerificationEmail.calls.all()[0].args[0].link;
|
||||
const linkWithLocale = new URL(link);
|
||||
@@ -825,15 +844,15 @@ describe('Pages Router', () => {
|
||||
|
||||
const appId = linkResponse.headers['x-parse-page-param-appid'];
|
||||
const locale = linkResponse.headers['x-parse-page-param-locale'];
|
||||
const username = linkResponse.headers['x-parse-page-param-username'];
|
||||
const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl'];
|
||||
await jasmine.timeout();
|
||||
|
||||
const invalidVerificationPagePath = pageResponse.calls.all()[0].args[0];
|
||||
expect(appId).toBeDefined();
|
||||
expect(locale).toBe(exampleLocale);
|
||||
expect(username).toBeDefined();
|
||||
expect(publicServerUrl).toBeDefined();
|
||||
expect(invalidVerificationPagePath).toMatch(
|
||||
new RegExp(`\/${exampleLocale}\/${pages.emailVerificationLinkInvalid.defaultFile}`)
|
||||
new RegExp(`\/${exampleLocale}\/${pages.emailVerificationLinkExpired.defaultFile}`)
|
||||
);
|
||||
|
||||
spyOn(UserController.prototype, 'resendVerificationEmail').and.callFake(() =>
|
||||
@@ -846,75 +865,12 @@ describe('Pages Router', () => {
|
||||
method: 'POST',
|
||||
body: {
|
||||
locale,
|
||||
username: 'exampleUsername',
|
||||
username,
|
||||
},
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
followRedirects: false,
|
||||
});
|
||||
expect(formResponse.status).toEqual(303);
|
||||
// With emailVerifySuccessOnInvalidEmail: true (default), the resend
|
||||
// page always redirects to the success page to prevent user enumeration
|
||||
expect(formResponse.text).toContain(
|
||||
`/${locale}/${pages.emailVerificationSendSuccess.defaultFile}`
|
||||
);
|
||||
});
|
||||
|
||||
it('localizes end-to-end for verify email: invalid verification link - link send fail with emailVerifySuccessOnInvalidEmail disabled', async () => {
|
||||
config.emailVerifySuccessOnInvalidEmail = false;
|
||||
await reconfigureServer(config);
|
||||
const sendVerificationEmail = spyOn(
|
||||
config.emailAdapter,
|
||||
'sendVerificationEmail'
|
||||
).and.callThrough();
|
||||
const user = new Parse.User();
|
||||
user.setUsername('exampleUsername');
|
||||
user.setPassword('examplePassword');
|
||||
user.set('email', 'mail@example.com');
|
||||
await user.signUp();
|
||||
await jasmine.timeout();
|
||||
|
||||
const link = sendVerificationEmail.calls.all()[0].args[0].link;
|
||||
const linkWithLocale = new URL(link);
|
||||
linkWithLocale.searchParams.append(pageParams.locale, exampleLocale);
|
||||
linkWithLocale.searchParams.set(pageParams.token, 'invalidToken');
|
||||
|
||||
const linkResponse = await request({
|
||||
url: linkWithLocale.toString(),
|
||||
followRedirects: false,
|
||||
});
|
||||
expect(linkResponse.status).toBe(200);
|
||||
|
||||
const appId = linkResponse.headers['x-parse-page-param-appid'];
|
||||
const locale = linkResponse.headers['x-parse-page-param-locale'];
|
||||
const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl'];
|
||||
await jasmine.timeout();
|
||||
|
||||
const invalidVerificationPagePath = pageResponse.calls.all()[0].args[0];
|
||||
expect(appId).toBeDefined();
|
||||
expect(locale).toBe(exampleLocale);
|
||||
expect(publicServerUrl).toBeDefined();
|
||||
expect(invalidVerificationPagePath).toMatch(
|
||||
new RegExp(`\/${exampleLocale}\/${pages.emailVerificationLinkInvalid.defaultFile}`)
|
||||
);
|
||||
|
||||
spyOn(UserController.prototype, 'resendVerificationEmail').and.callFake(() =>
|
||||
Promise.reject('failed to resend verification email')
|
||||
);
|
||||
|
||||
const formUrl = `${publicServerUrl}/apps/${appId}/resend_verification_email`;
|
||||
const formResponse = await request({
|
||||
url: formUrl,
|
||||
method: 'POST',
|
||||
body: {
|
||||
locale,
|
||||
username: 'exampleUsername',
|
||||
},
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
followRedirects: false,
|
||||
});
|
||||
expect(formResponse.status).toEqual(303);
|
||||
// With emailVerifySuccessOnInvalidEmail: false, the resend page
|
||||
// redirects to the fail page
|
||||
expect(formResponse.text).toContain(
|
||||
`/${locale}/${pages.emailVerificationSendFail.defaultFile}`
|
||||
);
|
||||
@@ -1021,125 +977,6 @@ describe('Pages Router', () => {
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.text).toBe('Not found.');
|
||||
});
|
||||
|
||||
it('rejects requesting file from sibling directory with prefix-colliding name via encoded path traversal', async () => {
|
||||
// Create a temporary pages directory and a sibling directory whose name
|
||||
// starts with the same prefix (e.g. "pages" vs "pages-secret"), which
|
||||
// would bypass a naive `startsWith` check without a path separator.
|
||||
const baseDir = path.join(__dirname, 'tmp-pages-exploit-test');
|
||||
const pagesDir = path.join(baseDir, 'pages');
|
||||
const siblingDir = path.join(baseDir, 'pages-secret');
|
||||
const marker = `SECRET_CONTENT_${Date.now()}`;
|
||||
|
||||
try {
|
||||
await fs.mkdir(pagesDir, { recursive: true });
|
||||
await fs.mkdir(siblingDir, { recursive: true });
|
||||
// Copy a required HTML file so the pages router initializes correctly
|
||||
const publicDir = path.resolve(__dirname, '../public');
|
||||
const htmlFile = await fs.readFile(
|
||||
path.join(publicDir, 'email_verification_link_invalid.html'),
|
||||
'utf-8'
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(pagesDir, 'email_verification_link_invalid.html'),
|
||||
htmlFile
|
||||
);
|
||||
// Write a secret file in the sibling directory
|
||||
await fs.writeFile(path.join(siblingDir, 'secret.txt'), marker);
|
||||
|
||||
config.pages.pagesPath = pagesDir;
|
||||
await reconfigureServer(config);
|
||||
|
||||
// Use URL-encoded path traversal: %2e%2e%2f = ../
|
||||
// This reaches the sibling "pages-secret" directory which shares
|
||||
// the "pages" prefix with the configured pagesPath directory name.
|
||||
const url = `${config.publicServerURL}/apps/%2e%2e%2fpages-secret%2fsecret.txt`;
|
||||
const response = await request({
|
||||
url: url,
|
||||
followRedirects: false,
|
||||
}).catch(e => e);
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.text).not.toContain(marker);
|
||||
} finally {
|
||||
await fs.rm(baseDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('does not leak email verification status via resend page when emailVerifySuccessOnInvalidEmail is true', async () => {
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: () => {},
|
||||
sendPasswordResetEmail: () => {},
|
||||
sendMail: () => {},
|
||||
};
|
||||
await reconfigureServer({
|
||||
...config,
|
||||
verifyUserEmails: true,
|
||||
emailVerifySuccessOnInvalidEmail: true,
|
||||
emailAdapter,
|
||||
});
|
||||
|
||||
// Create a user with unverified email
|
||||
const user = new Parse.User();
|
||||
user.setUsername('realuser');
|
||||
user.setPassword('password123');
|
||||
user.setEmail('real@example.com');
|
||||
await user.signUp();
|
||||
|
||||
const formUrl = `${config.publicServerURL}/apps/${config.appId}/resend_verification_email`;
|
||||
|
||||
// Resend for existing unverified user
|
||||
const existingResponse = await request({
|
||||
method: 'POST',
|
||||
url: formUrl,
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: 'username=realuser',
|
||||
followRedirects: false,
|
||||
}).catch(e => e);
|
||||
|
||||
// Resend for non-existing user
|
||||
const nonExistingResponse = await request({
|
||||
method: 'POST',
|
||||
url: formUrl,
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: 'username=fakeuser',
|
||||
followRedirects: false,
|
||||
}).catch(e => e);
|
||||
|
||||
// Both should redirect to the same page (success) to prevent enumeration
|
||||
expect(existingResponse.status).toBe(303);
|
||||
expect(nonExistingResponse.status).toBe(303);
|
||||
expect(existingResponse.headers.location).toContain('email_verification_send_success');
|
||||
expect(nonExistingResponse.headers.location).toContain('email_verification_send_success');
|
||||
});
|
||||
|
||||
it('does leak email verification status via resend page when emailVerifySuccessOnInvalidEmail is false', async () => {
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: () => {},
|
||||
sendPasswordResetEmail: () => {},
|
||||
sendMail: () => {},
|
||||
};
|
||||
await reconfigureServer({
|
||||
...config,
|
||||
verifyUserEmails: true,
|
||||
emailVerifySuccessOnInvalidEmail: false,
|
||||
emailAdapter,
|
||||
});
|
||||
|
||||
const formUrl = `${config.publicServerURL}/apps/${config.appId}/resend_verification_email`;
|
||||
|
||||
// Resend for non-existing user should redirect to fail page
|
||||
const nonExistingResponse = await request({
|
||||
method: 'POST',
|
||||
url: formUrl,
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: 'username=fakeuser',
|
||||
followRedirects: false,
|
||||
}).catch(e => e);
|
||||
|
||||
expect(nonExistingResponse.status).toBe(303);
|
||||
expect(nonExistingResponse.headers.location).toContain('email_verification_send_fail');
|
||||
});
|
||||
});
|
||||
|
||||
describe('custom route', () => {
|
||||
@@ -1313,10 +1150,12 @@ describe('Pages Router', () => {
|
||||
|
||||
const appId = linkResponse.headers['x-parse-page-param-appid'];
|
||||
const token = linkResponse.headers['x-parse-page-param-token'];
|
||||
const username = linkResponse.headers['x-parse-page-param-username'];
|
||||
const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl'];
|
||||
const passwordResetPagePath = pageResponse.calls.all()[0].args[0];
|
||||
expect(appId).toBeDefined();
|
||||
expect(token).toBeDefined();
|
||||
expect(username).toBeDefined();
|
||||
expect(publicServerUrl).toBeDefined();
|
||||
expect(passwordResetPagePath).toMatch(new RegExp(`\/${pages.passwordReset.defaultFile}`));
|
||||
pageResponse.calls.reset();
|
||||
@@ -1327,6 +1166,7 @@ describe('Pages Router', () => {
|
||||
method: 'POST',
|
||||
body: {
|
||||
token,
|
||||
username,
|
||||
new_password: 'newPassword',
|
||||
},
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
@@ -1338,7 +1178,7 @@ describe('Pages Router', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it_id('81c1c28e-5dfd-4ffb-a09b-283156c08483')(it)('email verification works with custom endpoint', async () => {
|
||||
it('email verification works with custom endpoint', async () => {
|
||||
config.pages.pagesEndpoint = 'customEndpoint';
|
||||
await reconfigureServer(config);
|
||||
const sendVerificationEmail = spyOn(
|
||||
@@ -1350,7 +1190,6 @@ describe('Pages Router', () => {
|
||||
user.setPassword('examplePassword');
|
||||
user.set('email', 'mail@example.com');
|
||||
await user.signUp();
|
||||
await jasmine.timeout();
|
||||
|
||||
const link = sendVerificationEmail.calls.all()[0].args[0].link;
|
||||
const linkResponse = await request({
|
||||
@@ -1358,77 +1197,10 @@ describe('Pages Router', () => {
|
||||
followRedirects: false,
|
||||
});
|
||||
expect(linkResponse.status).toBe(200);
|
||||
|
||||
const pagePath = pageResponse.calls.all()[0].args[0];
|
||||
expect(pagePath).toMatch(new RegExp(`\/${pages.emailVerificationSuccess.defaultFile}`));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('XSS Protection', () => {
|
||||
beforeEach(async () => {
|
||||
await reconfigureServer({
|
||||
appId: 'test',
|
||||
appName: 'exampleAppname',
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
pages: { enableRouter: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('should escape XSS payloads in token parameter', async () => {
|
||||
const xssPayload = '"><script>alert("XSS")</script>';
|
||||
const response = await request({
|
||||
url: `http://localhost:8378/1/apps/choose_password?token=${encodeURIComponent(xssPayload)}&username=test&appId=test`,
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.text).not.toContain('<script>alert("XSS")</script>');
|
||||
expect(response.text).toContain('"><script>');
|
||||
});
|
||||
|
||||
it('should escape XSS in username parameter', async () => {
|
||||
const xssUsername = '<img src=x onerror=alert(1)>';
|
||||
const response = await request({
|
||||
url: `http://localhost:8378/1/apps/choose_password?username=${encodeURIComponent(xssUsername)}&appId=test`,
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.text).not.toContain('<img src=x onerror=alert(1)>');
|
||||
expect(response.text).toContain('<img');
|
||||
});
|
||||
|
||||
it('should escape XSS in locale parameter', async () => {
|
||||
const xssLocale = '"><svg/onload=alert(1)>';
|
||||
const response = await request({
|
||||
url: `http://localhost:8378/1/apps/choose_password?locale=${encodeURIComponent(xssLocale)}&appId=test`,
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.text).not.toContain('<svg/onload=alert(1)>');
|
||||
expect(response.text).toContain('"><svg');
|
||||
});
|
||||
|
||||
it('should handle legitimate usernames with quotes correctly', async () => {
|
||||
const username = "O'Brien";
|
||||
const response = await request({
|
||||
url: `http://localhost:8378/1/apps/choose_password?username=${encodeURIComponent(username)}&appId=test`,
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
// Should be properly escaped as HTML entity
|
||||
expect(response.text).toContain('O'Brien');
|
||||
// Should NOT contain unescaped quote that breaks HTML
|
||||
expect(response.text).not.toContain('value="O\'Brien"');
|
||||
});
|
||||
|
||||
it('should handle legitimate usernames with ampersands correctly', async () => {
|
||||
const username = 'Smith & Co';
|
||||
const response = await request({
|
||||
url: `http://localhost:8378/1/apps/choose_password?username=${encodeURIComponent(username)}&appId=test`,
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
// Should be properly escaped
|
||||
expect(response.text).toContain('Smith & Co');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
const request = require('../lib/request');
|
||||
|
||||
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
const pushCompleted = async pushId => {
|
||||
const query = new Parse.Query('_PushStatus');
|
||||
query.equalTo('objectId', pushId);
|
||||
let result = await query.first({ useMasterKey: true });
|
||||
while (!(result && result.get('status') === 'succeeded')) {
|
||||
await jasmine.timeout();
|
||||
await sleep(100);
|
||||
result = await query.first({ useMasterKey: true });
|
||||
}
|
||||
};
|
||||
@@ -111,7 +113,7 @@ const setup = function () {
|
||||
};
|
||||
|
||||
describe('Parse.Push', () => {
|
||||
it_id('d1e591c4-2b21-466b-9ee2-5be467b6b771')(it)('should properly send push', async () => {
|
||||
it('should properly send push', async () => {
|
||||
const { sendToInstallationSpy } = await setup();
|
||||
const pushStatusId = await Parse.Push.send({
|
||||
where: {
|
||||
@@ -126,7 +128,7 @@ describe('Parse.Push', () => {
|
||||
expect(sendToInstallationSpy.calls.count()).toEqual(10);
|
||||
});
|
||||
|
||||
it_id('2a58e3c7-b6f3-4261-a384-6c893b2ac3f3')(it)('should properly send push with lowercaseIncrement', async () => {
|
||||
it('should properly send push with lowercaseIncrement', async () => {
|
||||
await setup();
|
||||
const pushStatusId = await Parse.Push.send({
|
||||
where: {
|
||||
@@ -140,7 +142,7 @@ describe('Parse.Push', () => {
|
||||
await pushCompleted(pushStatusId);
|
||||
});
|
||||
|
||||
it_id('e21780b6-2cdd-467e-8013-81030f3288e9')(it)('should not allow clients to query _PushStatus', async () => {
|
||||
it('should not allow clients to query _PushStatus', async () => {
|
||||
await setup();
|
||||
const pushStatusId = await Parse.Push.send({
|
||||
where: {
|
||||
@@ -166,7 +168,7 @@ describe('Parse.Push', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it_id('924cf5f5-f684-4925-978a-e52c0c457366')(it)('should allow master key to query _PushStatus', async () => {
|
||||
it('should allow master key to query _PushStatus', async () => {
|
||||
await setup();
|
||||
const pushStatusId = await Parse.Push.send({
|
||||
where: {
|
||||
|
||||
+10
-30
@@ -5,9 +5,17 @@ const Config = require('../lib/Config');
|
||||
const auth = require('../lib/Auth');
|
||||
|
||||
describe('Parse.ACL', () => {
|
||||
it('acl must be valid', () => {
|
||||
it('acl must be valid', done => {
|
||||
const user = new Parse.User();
|
||||
expect(() => user.setACL('ACL')).toThrow(new Parse.Error(Parse.Error.OTHER_CAUSE, 'ACL must be a Parse ACL.'));
|
||||
ok(
|
||||
!user.setACL("Ceci n'est pas un ACL.", {
|
||||
error: function (user, error) {
|
||||
equal(error.code, -1);
|
||||
done();
|
||||
},
|
||||
}),
|
||||
'setACL should have returned false.'
|
||||
);
|
||||
});
|
||||
|
||||
it('refresh object with acl', async done => {
|
||||
@@ -923,32 +931,4 @@ describe('Parse.ACL', () => {
|
||||
|
||||
rest.create(config, auth.nobody(config), '_User', anonUser);
|
||||
});
|
||||
|
||||
it('support defaultACL in schema', async () => {
|
||||
await new Parse.Object('TestObject').save();
|
||||
const schema = await Parse.Server.database.loadSchema();
|
||||
await schema.updateClass(
|
||||
'TestObject',
|
||||
{},
|
||||
{
|
||||
create: {
|
||||
'*': true,
|
||||
},
|
||||
ACL: {
|
||||
'*': { read: true },
|
||||
currentUser: { read: true, write: true },
|
||||
},
|
||||
}
|
||||
);
|
||||
const acls = new Parse.ACL();
|
||||
acls.setPublicReadAccess(true);
|
||||
const user = await Parse.User.signUp('testuser', 'p@ssword');
|
||||
const obj = new Parse.Object('TestObject');
|
||||
await obj.save(null, { sessionToken: user.getSessionToken() });
|
||||
expect(obj.getACL()).toBeDefined();
|
||||
const acl = obj.getACL().toJSON();
|
||||
expect(acl['*']).toEqual({ read: true });
|
||||
expect(acl[user.id].write).toBeTrue();
|
||||
expect(acl[user.id].read).toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
+14
-19
@@ -6,7 +6,7 @@ const request = require('../lib/request');
|
||||
const Parse = require('parse/node');
|
||||
const Config = require('../lib/Config');
|
||||
const SchemaController = require('../lib/Controllers/SchemaController');
|
||||
const { destroyAllDataPermanently } = require('../lib/TestUtils');
|
||||
const TestUtils = require('../lib/TestUtils');
|
||||
|
||||
const userSchema = SchemaController.convertSchemaToAdapterSchema({
|
||||
className: '_User',
|
||||
@@ -33,7 +33,9 @@ describe('miscellaneous', () => {
|
||||
expect(results.length).toEqual(1);
|
||||
expect(results[0]['foo']).toEqual('bar');
|
||||
});
|
||||
});
|
||||
|
||||
describe('miscellaneous', function () {
|
||||
it('create a GameScore object', function (done) {
|
||||
const obj = new Parse.Object('GameScore');
|
||||
obj.set('score', 1337);
|
||||
@@ -161,7 +163,7 @@ describe('miscellaneous', () => {
|
||||
expect(numCreated).toBe(1);
|
||||
});
|
||||
|
||||
it_id('be1b9ac7-5e5f-4e91-b044-2bd8fb7622ad')(it)('ensure that if people already have duplicate users, they can still sign up new users', async done => {
|
||||
it('ensure that if people already have duplicate users, they can still sign up new users', async done => {
|
||||
try {
|
||||
await Parse.User.logOut();
|
||||
} catch (e) {
|
||||
@@ -169,7 +171,7 @@ describe('miscellaneous', () => {
|
||||
}
|
||||
const config = Config.get('test');
|
||||
// Remove existing data to clear out unique index
|
||||
destroyAllDataPermanently()
|
||||
TestUtils.destroyAllDataPermanently()
|
||||
.then(() => config.database.adapter.performInitialization({ VolatileClassesSchemas: [] }))
|
||||
.then(() => config.database.adapter.createClass('_User', userSchema))
|
||||
.then(() =>
|
||||
@@ -207,10 +209,10 @@ describe('miscellaneous', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_id('d00f907e-41b9-40f6-8168-63e832199a8c')(it)('ensure that if people already have duplicate emails, they can still sign up new users', done => {
|
||||
it('ensure that if people already have duplicate emails, they can still sign up new users', done => {
|
||||
const config = Config.get('test');
|
||||
// Remove existing data to clear out unique index
|
||||
destroyAllDataPermanently()
|
||||
TestUtils.destroyAllDataPermanently()
|
||||
.then(() => config.database.adapter.performInitialization({ VolatileClassesSchemas: [] }))
|
||||
.then(() => config.database.adapter.createClass('_User', userSchema))
|
||||
.then(() =>
|
||||
@@ -287,7 +289,7 @@ describe('miscellaneous', () => {
|
||||
}, fail);
|
||||
});
|
||||
|
||||
it_id('33db6efe-7c02-496c-8595-0ef627a94103')(it)('increment with a user object', function (done) {
|
||||
it('increment with a user object', function (done) {
|
||||
createTestUser()
|
||||
.then(user => {
|
||||
user.increment('foo');
|
||||
@@ -315,7 +317,7 @@ describe('miscellaneous', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it_id('bef99522-bcfd-4f79-ba9e-3c3845550401')(it)('save various data types', function (done) {
|
||||
it('save various data types', function (done) {
|
||||
const obj = new TestObject();
|
||||
obj.set('date', new Date());
|
||||
obj.set('array', [1, 2, 3]);
|
||||
@@ -949,7 +951,7 @@ describe('miscellaneous', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it_id('e9e718a9-4465-4158-b13e-f146855a8892')(it)('return the updated fields on PUT', async () => {
|
||||
it('return the updated fields on PUT', async () => {
|
||||
const obj = new Parse.Object('GameScore');
|
||||
const pointer = new Parse.Object('Child');
|
||||
await pointer.save();
|
||||
@@ -1021,7 +1023,7 @@ describe('miscellaneous', () => {
|
||||
expect(body.updatedAt).not.toBeUndefined();
|
||||
});
|
||||
|
||||
it_id('ea358b59-03c0-45c9-abc7-1fdd67573029')(it)('should response should not change with triggers', async () => {
|
||||
it('should response should not change with triggers', async () => {
|
||||
const obj = new Parse.Object('GameScore');
|
||||
const pointer = new Parse.Object('Child');
|
||||
Parse.Cloud.beforeSave('GameScore', request => {
|
||||
@@ -1266,7 +1268,6 @@ describe('miscellaneous', () => {
|
||||
});
|
||||
|
||||
it('test cloud function query parameters with array of pointers', async () => {
|
||||
await reconfigureServer({ encodeParseObjectInCloudFunction: false });
|
||||
Parse.Cloud.define('echoParams', req => {
|
||||
return req.params;
|
||||
});
|
||||
@@ -1284,7 +1285,6 @@ describe('miscellaneous', () => {
|
||||
const res = response.data.result;
|
||||
expect(res.arr.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('can handle null params in cloud functions (regression test for #1742)', done => {
|
||||
Parse.Cloud.define('func', request => {
|
||||
expect(request.params.nullParam).toEqual(null);
|
||||
@@ -1489,7 +1489,7 @@ describe('miscellaneous', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_id('b2cd9cf2-13fa-4acd-aaa9-6f81fc1858db')(it)('properly returns incremented values (#1554)', done => {
|
||||
it('properly returns incremented values (#1554)', done => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
@@ -1678,7 +1678,7 @@ describe('miscellaneous', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_id('8f99ee20-3da7-45ec-b867-ea0eb87524a9')(it)('purge all objects in class', done => {
|
||||
it('purge all objects in class', done => {
|
||||
const object = new Parse.Object('TestObject');
|
||||
object.set('foo', 'bar');
|
||||
const object2 = new Parse.Object('TestObject');
|
||||
@@ -1710,15 +1710,11 @@ describe('miscellaneous', () => {
|
||||
});
|
||||
|
||||
it('fail on purge all objects in class without master key', done => {
|
||||
const logger = require('../lib/logger').default;
|
||||
const loggerErrorSpy = spyOn(logger, 'error').and.callThrough();
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
loggerErrorSpy.calls.reset();
|
||||
request({
|
||||
method: 'DELETE',
|
||||
headers: headers,
|
||||
@@ -1728,8 +1724,7 @@ describe('miscellaneous', () => {
|
||||
fail('Should not succeed');
|
||||
})
|
||||
.catch(response => {
|
||||
expect(response.data.error).toEqual('Permission denied');
|
||||
expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required'));
|
||||
expect(response.data.error).toEqual('unauthorized: master key is required');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
const Config = require('../lib/Config');
|
||||
|
||||
describe('Config Keys', () => {
|
||||
const invalidKeyErrorMessage = 'Invalid key\\(s\\) found in Parse Server configuration';
|
||||
let loggerErrorSpy;
|
||||
|
||||
beforeEach(async () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
loggerErrorSpy = spyOn(logger, 'error').and.callThrough();
|
||||
spyOn(Config, 'validateOptions').and.callFake(() => {});
|
||||
});
|
||||
|
||||
it('recognizes invalid keys in root', async () => {
|
||||
await expectAsync(reconfigureServer({
|
||||
invalidKey: 1,
|
||||
})).toBeResolved();
|
||||
const error = loggerErrorSpy.calls.all().reduce((s, call) => s += call.args[0], '');
|
||||
expect(error).toMatch(invalidKeyErrorMessage);
|
||||
});
|
||||
|
||||
it('recognizes invalid keys in pages.customUrls', async () => {
|
||||
await expectAsync(reconfigureServer({
|
||||
pages: {
|
||||
customUrls: {
|
||||
invalidKey: 1,
|
||||
EmailVerificationSendFail: 1,
|
||||
}
|
||||
}
|
||||
})).toBeResolved();
|
||||
const error = loggerErrorSpy.calls.all().reduce((s, call) => s += call.args[0], '');
|
||||
expect(error).toMatch(invalidKeyErrorMessage);
|
||||
expect(error).toMatch(`invalidKey`);
|
||||
expect(error).toMatch(`EmailVerificationSendFail`);
|
||||
});
|
||||
|
||||
it('recognizes invalid keys in liveQueryServerOptions', async () => {
|
||||
await expectAsync(reconfigureServer({
|
||||
liveQueryServerOptions: {
|
||||
invalidKey: 1,
|
||||
MasterKey: 1,
|
||||
}
|
||||
})).toBeResolved();
|
||||
const error = loggerErrorSpy.calls.all().reduce((s, call) => s += call.args[0], '');
|
||||
expect(error).toMatch(invalidKeyErrorMessage);
|
||||
expect(error).toMatch(`MasterKey`);
|
||||
});
|
||||
|
||||
it('recognizes invalid keys in rateLimit', async () => {
|
||||
await expectAsync(reconfigureServer({
|
||||
rateLimit: [
|
||||
{ invalidKey: 1 },
|
||||
{ RequestPath: 1 },
|
||||
{ RequestTimeWindow: 1 },
|
||||
]
|
||||
})).toBeRejected();
|
||||
const error = loggerErrorSpy.calls.all().reduce((s, call) => s += call.args[0], '');
|
||||
expect(error).toMatch(invalidKeyErrorMessage);
|
||||
expect(error).toMatch('rateLimit\\[0\\]\\.invalidKey');
|
||||
expect(error).toMatch('rateLimit\\[1\\]\\.RequestPath');
|
||||
expect(error).toMatch('rateLimit\\[2\\]\\.RequestTimeWindow');
|
||||
});
|
||||
|
||||
it_only_db('mongo')('recognizes valid keys in default configuration', async () => {
|
||||
await expectAsync(reconfigureServer({
|
||||
...defaultConfiguration,
|
||||
})).toBeResolved();
|
||||
expect(loggerErrorSpy.calls.all().reduce((s, call) => s += call.args[0], '')).not.toMatch(invalidKeyErrorMessage);
|
||||
});
|
||||
|
||||
it_only_db('mongo')('recognizes valid keys in databaseOptions (MongoDB)', async () => {
|
||||
await expectAsync(reconfigureServer({
|
||||
databaseURI: 'mongodb://localhost:27017/parse',
|
||||
filesAdapter: null,
|
||||
databaseAdapter: null,
|
||||
databaseOptions: {
|
||||
appName: 'MyParseApp',
|
||||
|
||||
// Cannot be tested as it requires authentication setup
|
||||
// authMechanism: 'SCRAM-SHA-256',
|
||||
// authMechanismProperties: { SERVICE_NAME: 'mongodb' },
|
||||
|
||||
authSource: 'admin',
|
||||
autoSelectFamily: true,
|
||||
autoSelectFamilyAttemptTimeout: 3000,
|
||||
compressors: ['zlib'],
|
||||
connectTimeoutMS: 5000,
|
||||
directConnection: false,
|
||||
disableIndexFieldValidation: true,
|
||||
forceServerObjectId: false,
|
||||
heartbeatFrequencyMS: 10000,
|
||||
localThresholdMS: 15,
|
||||
maxConnecting: 2,
|
||||
maxIdleTimeMS: 60000,
|
||||
maxPoolSize: 10,
|
||||
maxStalenessSeconds: 90,
|
||||
maxTimeMS: 1000,
|
||||
minPoolSize: 5,
|
||||
|
||||
// Cannot be tested as it requires a proxy setup
|
||||
// proxyHost: 'proxy.example.com',
|
||||
// proxyPassword: 'proxypass',
|
||||
// proxyPort: 1080,
|
||||
// proxyUsername: 'proxyuser',
|
||||
|
||||
readConcernLevel: 'majority',
|
||||
readPreference: 'secondaryPreferred',
|
||||
readPreferenceTags: [{ dc: 'east' }],
|
||||
|
||||
// Cannot be tested as it requires a replica set setup
|
||||
// replicaSet: 'myReplicaSet',
|
||||
|
||||
retryReads: true,
|
||||
retryWrites: true,
|
||||
serverMonitoringMode: 'auto',
|
||||
serverSelectionTimeoutMS: 5000,
|
||||
socketTimeoutMS: 5000,
|
||||
|
||||
// Cannot be tested as it requires a replica cluster setup
|
||||
// srvMaxHosts: 0,
|
||||
// srvServiceName: 'mongodb',
|
||||
|
||||
ssl: false,
|
||||
tls: false,
|
||||
tlsAllowInvalidCertificates: false,
|
||||
tlsAllowInvalidHostnames: false,
|
||||
tlsCAFile: __dirname + '/support/cert/cert.pem',
|
||||
tlsCertificateKeyFile: __dirname + '/support/cert/cert.pem',
|
||||
tlsCertificateKeyFilePassword: 'password',
|
||||
waitQueueTimeoutMS: 5000,
|
||||
zlibCompressionLevel: 6,
|
||||
},
|
||||
})).toBeResolved();
|
||||
await expectAsync(reconfigureServer({
|
||||
databaseURI: 'mongodb://localhost:27017/parse',
|
||||
filesAdapter: null,
|
||||
databaseAdapter: null,
|
||||
databaseOptions: {
|
||||
// The following option needs to be tested separately due to driver config rules
|
||||
tlsInsecure: false,
|
||||
},
|
||||
})).toBeResolved();
|
||||
expect(loggerErrorSpy.calls.all().reduce((s, call) => s += call.args[0], '')).not.toMatch(invalidKeyErrorMessage);
|
||||
});
|
||||
});
|
||||
+50
-176
@@ -13,13 +13,6 @@ for (let i = 0; i < str.length; i++) {
|
||||
}
|
||||
|
||||
describe('Parse.File testing', () => {
|
||||
let loggerErrorSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
const logger = require('../lib/logger').default;
|
||||
loggerErrorSpy = spyOn(logger, 'error').and.callThrough();
|
||||
});
|
||||
|
||||
describe('creating files', () => {
|
||||
it('works with Content-Type', done => {
|
||||
const headers = {
|
||||
@@ -153,7 +146,6 @@ describe('Parse.File testing', () => {
|
||||
const b = response.data;
|
||||
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*thefile.jpg$/);
|
||||
// missing X-Parse-Master-Key header
|
||||
loggerErrorSpy.calls.reset();
|
||||
request({
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
@@ -164,10 +156,8 @@ describe('Parse.File testing', () => {
|
||||
}).then(fail, response => {
|
||||
const del_b = response.data;
|
||||
expect(response.status).toEqual(403);
|
||||
expect(del_b.error).toBe('Permission denied');
|
||||
expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required'));
|
||||
expect(del_b.error).toMatch(/unauthorized/);
|
||||
// incorrect X-Parse-Master-Key header
|
||||
loggerErrorSpy.calls.reset();
|
||||
request({
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
@@ -179,8 +169,7 @@ describe('Parse.File testing', () => {
|
||||
}).then(fail, response => {
|
||||
const del_b2 = response.data;
|
||||
expect(response.status).toEqual(403);
|
||||
expect(del_b2.error).toBe('Permission denied');
|
||||
expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required'));
|
||||
expect(del_b2.error).toMatch(/unauthorized/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -389,26 +378,6 @@ describe('Parse.File testing', () => {
|
||||
expect(response.headers['content-type']).toMatch(/^text\/html/);
|
||||
});
|
||||
|
||||
it('works without Content-Type and extension', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
},
|
||||
});
|
||||
const headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
const result = await request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/files/file',
|
||||
body: '<html></html>\n',
|
||||
});
|
||||
expect(result.data.url.includes('file.txt')).toBeTrue();
|
||||
expect(result.data.name.includes('file.txt')).toBeTrue();
|
||||
});
|
||||
|
||||
it('filename is url encoded', done => {
|
||||
const headers = {
|
||||
'Content-Type': 'text/html',
|
||||
@@ -664,80 +633,6 @@ describe('Parse.File testing', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('URI-backed file upload is disabled to prevent SSRF attack', () => {
|
||||
const express = require('express');
|
||||
let testServer;
|
||||
let testServerPort;
|
||||
let requestsMade;
|
||||
|
||||
beforeEach(async () => {
|
||||
requestsMade = [];
|
||||
const app = express();
|
||||
app.use((req, res) => {
|
||||
requestsMade.push({ url: req.url, method: req.method });
|
||||
res.status(200).send('test file content');
|
||||
});
|
||||
testServer = app.listen(0);
|
||||
testServerPort = testServer.address().port;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (testServer) {
|
||||
await new Promise(resolve => testServer.close(resolve));
|
||||
}
|
||||
Parse.Cloud._removeAllHooks();
|
||||
});
|
||||
|
||||
it('does not access URI when file upload attempted over REST', async () => {
|
||||
const response = await request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/classes/TestClass',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
body: {
|
||||
file: {
|
||||
__type: 'File',
|
||||
name: 'test.txt',
|
||||
_source: {
|
||||
format: 'uri',
|
||||
uri: `http://127.0.0.1:${testServerPort}/secret-file.txt`,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(response.status).toBe(201);
|
||||
// Verify no HTTP request was made to the URI
|
||||
expect(requestsMade.length).toBe(0);
|
||||
});
|
||||
|
||||
it('does not access URI when file created in beforeSave trigger', async () => {
|
||||
Parse.Cloud.beforeSave(Parse.File, () => {
|
||||
return new Parse.File('trigger-file.txt', {
|
||||
uri: `http://127.0.0.1:${testServerPort}/secret-file.txt`,
|
||||
});
|
||||
});
|
||||
await expectAsync(
|
||||
request({
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
url: 'http://localhost:8378/1/files/test.txt',
|
||||
body: 'test content',
|
||||
})
|
||||
).toBeRejectedWith(jasmine.objectContaining({
|
||||
status: 400
|
||||
}));
|
||||
// Verify no HTTP request was made to the URI
|
||||
expect(requestsMade.length).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleting files', () => {
|
||||
@@ -894,11 +789,59 @@ describe('Parse.File testing', () => {
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
Range: 'bytes=abc-efs',
|
||||
},
|
||||
}).catch(e => e);
|
||||
expect(file.headers['content-range']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('supports bytes range if start and end undefined', async () => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
const response = await request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1//files/file.txt ',
|
||||
body: repeat('argle bargle', 100),
|
||||
});
|
||||
const b = response.data;
|
||||
const file = await request({
|
||||
url: b.url,
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
},
|
||||
}).catch(e => e);
|
||||
expect(file.headers['content-range']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('supports bytes range if end is greater than size', async () => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
const response = await request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1//files/file.txt ',
|
||||
body: repeat('argle bargle', 100),
|
||||
});
|
||||
const b = response.data;
|
||||
const file = await request({
|
||||
url: b.url,
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
Range: 'bytes=0-2000',
|
||||
},
|
||||
}).catch(e => e);
|
||||
expect(file.headers['content-range']).toBe('bytes 0-1212/1212');
|
||||
});
|
||||
|
||||
it('supports bytes range if end is greater than size', async () => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
@@ -1421,75 +1364,6 @@ describe('Parse.File testing', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('default should allow common types', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
},
|
||||
});
|
||||
for (const type of ['plain', 'txt', 'png', 'jpg', 'gif', 'doc']) {
|
||||
const file = new Parse.File(`parse-server-logo.${type}`, { base64: 'ParseA==' });
|
||||
await file.save();
|
||||
}
|
||||
});
|
||||
|
||||
it('default should block SVG files', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
},
|
||||
});
|
||||
const headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
const svgContent = Buffer.from('<svg xmlns="http://www.w3.org/2000/svg"><script>alert(1)</script></svg>').toString('base64');
|
||||
for (const extension of ['svg', 'SVG', 'Svg']) {
|
||||
await expectAsync(
|
||||
request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: `http://localhost:8378/1/files/malicious.${extension}`,
|
||||
body: JSON.stringify({
|
||||
_ApplicationId: 'test',
|
||||
_JavaScriptKey: 'test',
|
||||
_ContentType: 'image/svg+xml',
|
||||
base64: svgContent,
|
||||
}),
|
||||
}).catch(e => {
|
||||
throw new Error(e.data.error);
|
||||
})
|
||||
).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension ${extension} is disabled.`)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('default should block SVG content type without file extension', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
},
|
||||
});
|
||||
const svgContent = Buffer.from('<svg xmlns="http://www.w3.org/2000/svg"><script>alert(1)</script></svg>').toString('base64');
|
||||
await expectAsync(
|
||||
request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/files/file',
|
||||
body: JSON.stringify({
|
||||
_ApplicationId: 'test',
|
||||
_JavaScriptKey: 'test',
|
||||
_ContentType: 'image/svg+xml',
|
||||
base64: svgContent,
|
||||
}),
|
||||
}).catch(e => {
|
||||
throw new Error(e.data.error);
|
||||
})
|
||||
).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension svg+xml is disabled.`)
|
||||
);
|
||||
});
|
||||
|
||||
it('works with a period in the file name', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
|
||||
+11
-11
@@ -93,7 +93,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it_id('bbd9e2f6-7f61-458f-98f2-4a563586cd8d')(it)('geo line', async done => {
|
||||
it('geo line', async done => {
|
||||
const line = [];
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
const obj = new TestObject();
|
||||
@@ -143,7 +143,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it_id('e1e86b38-b8a4-4109-8330-a324fe628e0c')(it)('geo max distance medium', async () => {
|
||||
it('geo max distance medium', async () => {
|
||||
const objects = [];
|
||||
[0, 1, 2].map(function (i) {
|
||||
const obj = new TestObject();
|
||||
@@ -207,7 +207,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it_id('05f1a454-56b1-4f2e-908e-408a9222cbae')(it)('geo max distance in km california', async () => {
|
||||
it('geo max distance in km california', async () => {
|
||||
await makeSomeGeoPoints();
|
||||
const sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
|
||||
const query = new Parse.Query(TestObject);
|
||||
@@ -246,7 +246,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
equal(results.length, 3);
|
||||
});
|
||||
|
||||
it_id('9ee376ad-dd6c-4c17-ad28-c7899a4411f1')(it)('geo max distance in miles california', async () => {
|
||||
it('geo max distance in miles california', async () => {
|
||||
await makeSomeGeoPoints();
|
||||
const sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
|
||||
const query = new Parse.Query(TestObject);
|
||||
@@ -276,7 +276,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
equal(results.length, 0);
|
||||
});
|
||||
|
||||
it_id('9e35a89e-bc2c-4ec5-b25a-8d1890a55233')(it)('returns nearest location', async () => {
|
||||
it('returns nearest location', async () => {
|
||||
await makeSomeGeoPoints();
|
||||
const sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
|
||||
const query = new Parse.Query(TestObject);
|
||||
@@ -286,7 +286,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
equal(results[1].get('name'), 'Sacramento');
|
||||
});
|
||||
|
||||
it_id('6df434b0-142d-4302-bbc6-a6ec5a9d9c68')(it)('works with geobox queries', done => {
|
||||
it('works with geobox queries', done => {
|
||||
const inbound = new Parse.GeoPoint(1.5, 1.5);
|
||||
const onbound = new Parse.GeoPoint(10, 10);
|
||||
const outbound = new Parse.GeoPoint(20, 20);
|
||||
@@ -356,7 +356,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_id('d9fbc5c6-f767-47d6-bb44-3858eb9df15a')(it)('supports withinPolygon open path', done => {
|
||||
it('supports withinPolygon open path', done => {
|
||||
const inbound = new Parse.GeoPoint(1.5, 1.5);
|
||||
const onbound = new Parse.GeoPoint(10, 10);
|
||||
const outbound = new Parse.GeoPoint(20, 20);
|
||||
@@ -394,7 +394,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
}, done.fail);
|
||||
});
|
||||
|
||||
it_id('3ec537bd-839a-4c93-a48b-b4a249820074')(it)('supports withinPolygon closed path', done => {
|
||||
it('supports withinPolygon closed path', done => {
|
||||
const inbound = new Parse.GeoPoint(1.5, 1.5);
|
||||
const onbound = new Parse.GeoPoint(10, 10);
|
||||
const outbound = new Parse.GeoPoint(20, 20);
|
||||
@@ -433,7 +433,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
}, done.fail);
|
||||
});
|
||||
|
||||
it_id('0a248e11-3598-480a-9ab5-8a0b259258e4')(it)('supports withinPolygon Polygon object', done => {
|
||||
it('supports withinPolygon Polygon object', done => {
|
||||
const inbound = new Parse.GeoPoint(1.5, 1.5);
|
||||
const onbound = new Parse.GeoPoint(10, 10);
|
||||
const outbound = new Parse.GeoPoint(20, 20);
|
||||
@@ -752,7 +752,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
equal(count, 1);
|
||||
});
|
||||
|
||||
it_id('0b073d31-0d41-41e7-bd60-f636ffb759dc')(it)('withinKilometers complex supports count', async () => {
|
||||
it('withinKilometers complex supports count', async () => {
|
||||
const inside = new Parse.GeoPoint(10, 10);
|
||||
const middle = new Parse.GeoPoint(20, 20);
|
||||
const outside = new Parse.GeoPoint(30, 30);
|
||||
@@ -770,7 +770,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
equal(count, 2);
|
||||
});
|
||||
|
||||
it_id('26c9a13d-3d71-452e-a91c-9a4589be021c')(it)('fails to fetch geopoints that are specifically not at (0,0)', async () => {
|
||||
it('fails to fetch geopoints that are specifically not at (0,0)', async () => {
|
||||
const tmp = new TestObject({
|
||||
location: new Parse.GeoPoint({ latitude: 0, longitude: 0 }),
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ const request = require('../lib/request');
|
||||
const Config = require('../lib/Config');
|
||||
|
||||
describe('a GlobalConfig', () => {
|
||||
beforeEach(async () => {
|
||||
beforeEach(done => {
|
||||
const config = Config.get('test');
|
||||
const query = on_db(
|
||||
'mongo',
|
||||
@@ -16,7 +16,7 @@ describe('a GlobalConfig', () => {
|
||||
return { objectId: '1' };
|
||||
}
|
||||
);
|
||||
await config.database.adapter
|
||||
config.database.adapter
|
||||
.upsertOneObject(
|
||||
'_GlobalConfig',
|
||||
{
|
||||
@@ -28,10 +28,14 @@ describe('a GlobalConfig', () => {
|
||||
},
|
||||
query,
|
||||
{
|
||||
params: { companies: ['US', 'DK'], counter: 20, internalParam: 'internal' },
|
||||
params: { companies: ['US', 'DK'], internalParam: 'internal' },
|
||||
masterKeyOnly: { internalParam: true },
|
||||
}
|
||||
);
|
||||
)
|
||||
.then(done, err => {
|
||||
jfail(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
const headers = {
|
||||
@@ -110,34 +114,6 @@ describe('a GlobalConfig', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_only_db('mongo')('can addUnique', async () => {
|
||||
await Parse.Config.save({ companies: { __op: 'AddUnique', objects: ['PA', 'RS', 'E'] } });
|
||||
const config = await Parse.Config.get();
|
||||
const companies = config.get('companies');
|
||||
expect(companies).toEqual(['US', 'DK', 'PA', 'RS', 'E']);
|
||||
});
|
||||
|
||||
it_only_db('mongo')('can add to array', async () => {
|
||||
await Parse.Config.save({ companies: { __op: 'Add', objects: ['PA'] } });
|
||||
const config = await Parse.Config.get();
|
||||
const companies = config.get('companies');
|
||||
expect(companies).toEqual(['US', 'DK', 'PA']);
|
||||
});
|
||||
|
||||
it_only_db('mongo')('can remove from array', async () => {
|
||||
await Parse.Config.save({ companies: { __op: 'Remove', objects: ['US'] } });
|
||||
const config = await Parse.Config.get();
|
||||
const companies = config.get('companies');
|
||||
expect(companies).toEqual(['DK']);
|
||||
});
|
||||
|
||||
it('can increment', async () => {
|
||||
await Parse.Config.save({ counter: { __op: 'Increment', amount: 49 } });
|
||||
const config = await Parse.Config.get();
|
||||
const counter = config.get('counter');
|
||||
expect(counter).toEqual(69);
|
||||
});
|
||||
|
||||
it('can add and retrive files', done => {
|
||||
request({
|
||||
method: 'PUT',
|
||||
@@ -181,7 +157,7 @@ describe('a GlobalConfig', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_id('5ebbd0cf-d1a5-49d9-aac7-5216abc5cb62')(it)('properly handles delete op', done => {
|
||||
it('properly handles delete op', done => {
|
||||
request({
|
||||
method: 'PUT',
|
||||
url: 'http://localhost:8378/1/config',
|
||||
@@ -189,7 +165,6 @@ describe('a GlobalConfig', () => {
|
||||
body: {
|
||||
params: {
|
||||
companies: { __op: 'Delete' },
|
||||
counter: { __op: 'Delete' },
|
||||
internalParam: { __op: 'Delete' },
|
||||
foo: 'bar',
|
||||
},
|
||||
@@ -208,7 +183,6 @@ describe('a GlobalConfig', () => {
|
||||
try {
|
||||
expect(response.status).toEqual(200);
|
||||
expect(body.params.companies).toBeUndefined();
|
||||
expect(body.params.counter).toBeUndefined();
|
||||
expect(body.params.foo).toBe('bar');
|
||||
expect(Object.keys(body.params).length).toBe(1);
|
||||
} catch (e) {
|
||||
@@ -220,9 +194,6 @@ describe('a GlobalConfig', () => {
|
||||
});
|
||||
|
||||
it('fail to update if master key is missing', done => {
|
||||
const logger = require('../lib/logger').default;
|
||||
const loggerErrorSpy = spyOn(logger, 'error').and.callThrough();
|
||||
loggerErrorSpy.calls.reset();
|
||||
request({
|
||||
method: 'PUT',
|
||||
url: 'http://localhost:8378/1/config',
|
||||
@@ -236,8 +207,7 @@ describe('a GlobalConfig', () => {
|
||||
}).then(fail, response => {
|
||||
const body = response.data;
|
||||
expect(response.status).toEqual(403);
|
||||
expect(body.error).toEqual('Permission denied');
|
||||
expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('unauthorized: master key is required'));
|
||||
expect(body.error).toEqual('unauthorized: master key is required');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -500,7 +500,7 @@ describe('ParseGraphQLSchema', () => {
|
||||
});
|
||||
});
|
||||
describe('alias', () => {
|
||||
it_id('45282d26-f4c7-4d2d-a7b6-cd8741d5322f')(it)('Should be able to define alias for get and find query', async () => {
|
||||
it('Should be able to define alias for get and find query', async () => {
|
||||
const parseGraphQLSchema = new ParseGraphQLSchema({
|
||||
databaseController,
|
||||
parseGraphQLController,
|
||||
@@ -535,7 +535,7 @@ describe('ParseGraphQLSchema', () => {
|
||||
expect(Object.keys(queries1)).toContain('precious_data');
|
||||
});
|
||||
|
||||
it_id('f04b46e3-a25d-401d-a315-3298cfee1df8')(it)('Should be able to define alias for mutation', async () => {
|
||||
it('Should be able to define alias for mutation', async () => {
|
||||
const parseGraphQLSchema = new ParseGraphQLSchema({
|
||||
databaseController,
|
||||
parseGraphQLController,
|
||||
|
||||
+106
-778
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